From 75d246e03c503b70027dd7471a8008a461da59cd Mon Sep 17 00:00:00 2001 From: AleksanderBodurri Date: Sun, 15 Jun 2025 19:10:52 -0400 Subject: [PATCH] test(devtools): revive cypress tests (#61972) Previously these tests would run automatically when Angular DevTools lived in another repo. These files have continued to live here but have not been running automatically on each PR. Now, these test files have been revived to run properly with our changes since the repo merge. This is a first step to reviving our e2e testing. Next steps include writing cypress tests for new features like Injector Graph, Router tree, signals visualizations, etc. PR Close #61972 --- .github/workflows/ci.yml | 9 +++ .github/workflows/pr.yml | 9 +++ devtools/cypress/cypress.config.js | 15 ++++ devtools/cypress/integration/base.e2e.js | 12 ++- .../cypress/integration/comment-nodes.e2e.js | 6 +- devtools/cypress/integration/elements.e2e.js | 5 +- .../cypress/integration/item-tracking.e2e.js | 12 ++- .../cypress/integration/node-search.e2e.js | 21 ++--- .../cypress/integration/node-selection.e2e.js | 44 ++++++----- .../cypress/integration/property-edit.e2e.js | 79 +++++++------------ .../integration/property-update.e2e.js | 6 +- .../view-component-metadata.e2e.js | 32 ++++---- devtools/cypress/support/commands.js | 47 ++++++----- .../property-view-body.component.html | 2 +- .../src/app/demo-app/demo-app.component.html | 1 + .../src/app/demo-app/demo-app.component.ts | 23 +++++- .../demo-app/todo/home/todos.component.html | 2 +- .../app/demo-app/todo/home/todos.component.ts | 2 + 18 files changed, 179 insertions(+), 148 deletions(-) create mode 100644 devtools/cypress/cypress.config.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f93e99efaf..085b4c91d9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,15 @@ jobs: run: yarn devtools:test - name: Test build run: yarn devtools:build:chrome + - name: Install Cypress + run: yarn global add cypress@14.4.1 + - name: Cypress run + uses: cypress-io/github-action@6c143abc292aa835d827652c2ea025d098311070 # v6.10.1 + with: + command: cypress run --project ./devtools/cypress + start: yarn bazel run //devtools/src:devserver + wait-on: 'http://localhost:4200' + wait-on-timeout: 300 test: runs-on: ubuntu-latest-4core diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 37de12e3783..fe60c649164 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -60,6 +60,15 @@ jobs: run: yarn devtools:test - name: Test build run: yarn devtools:build:chrome + - name: Install Cypress + run: yarn global add cypress@14.4.1 + - name: Cypress run + uses: cypress-io/github-action@6c143abc292aa835d827652c2ea025d098311070 # v6.10.1 + with: + command: cypress run --project ./devtools/cypress + start: yarn bazel run //devtools/src:devserver + wait-on: 'http://localhost:4200' + wait-on-timeout: 300 test: runs-on: ubuntu-latest-8core diff --git a/devtools/cypress/cypress.config.js b/devtools/cypress/cypress.config.js new file mode 100644 index 00000000000..706dc62d9fb --- /dev/null +++ b/devtools/cypress/cypress.config.js @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +module.exports = { + e2e: { + specPattern: 'integration/*.e2e.js', + supportFile: 'support/index.js', + baseUrl: 'http://localhost:4200', + }, +}; diff --git a/devtools/cypress/integration/base.e2e.js b/devtools/cypress/integration/base.e2e.js index 714f7bf4f88..c70b5610d45 100644 --- a/devtools/cypress/integration/base.e2e.js +++ b/devtools/cypress/integration/base.e2e.js @@ -6,15 +6,13 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('Testing the Todo app Demo', () => { beforeEach(() => { cy.visit('/'); }); it('should contain the todos application', () => { - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().contains('Todos'); getBody().contains('About'); getBody().contains('Clear completed'); @@ -23,15 +21,15 @@ describe('Testing the Todo app Demo', () => { }); it('should contain the "Components" tab', () => { - cy.contains('.mat-tab-links', 'Components'); + cy.contains('.devtools-nav', 'Components'); }); it('should contain the "Profiler" tab', () => { - cy.contains('.mat-tab-links', 'Profiler'); + cy.contains('.devtools-nav', 'Profiler'); }); it('should contain "app-root" and "app-todo-demo" in the component tree', () => { - cy.contains('.tree-node', 'app-root'); - cy.contains('.tree-node', 'app-todo-demo'); + cy.contains('ng-tree-node', 'app-root'); + cy.contains('ng-tree-node', 'app-todo-demo'); }); }); diff --git a/devtools/cypress/integration/comment-nodes.e2e.js b/devtools/cypress/integration/comment-nodes.e2e.js index 492118e1766..fff56712e91 100644 --- a/devtools/cypress/integration/comment-nodes.e2e.js +++ b/devtools/cypress/integration/comment-nodes.e2e.js @@ -8,7 +8,7 @@ function showComments() { cy.get('#nav-buttons > button:nth-child(2)').click(); - cy.get('#mat-slide-toggle-3 > label > div').click(); + cy.get('.cdk-overlay-container mat-slide-toggle label:contains("Show comment nodes")').click(); } describe('Comment nodes', () => { @@ -17,14 +17,14 @@ describe('Comment nodes', () => { }); it('should not find any comment nodes by default', () => { - const nodes = cy.$$('.tree-node:contains("#comment")'); + const nodes = cy.$$('ng-tree-node:contains("#comment")'); expect(nodes.length).to.eql(0); }); it('should find comment nodes when the setting is enabled', () => { showComments(); cy.get('.tree-wrapper') - .find('.tree-node:contains("#comment")') + .find('ng-tree-node:contains("#comment")') .its('length') .should('not.eq', 0); }); diff --git a/devtools/cypress/integration/elements.e2e.js b/devtools/cypress/integration/elements.e2e.js index 66d05401b33..711200894f2 100644 --- a/devtools/cypress/integration/elements.e2e.js +++ b/devtools/cypress/integration/elements.e2e.js @@ -12,6 +12,9 @@ describe('Angular Elements', () => { }); it('should recognize the zippy as an Angular Element', () => { - cy.get('.tree-wrapper').find('.tree-node:contains("app-zippy")').its('length').should('eq', 1); + cy.get('.tree-wrapper') + .find('ng-tree-node:contains("app-zippy")') + .its('length') + .should('eq', 1); }); }); diff --git a/devtools/cypress/integration/item-tracking.e2e.js b/devtools/cypress/integration/item-tracking.e2e.js index 99146a9d45b..36aca2fc639 100644 --- a/devtools/cypress/integration/item-tracking.e2e.js +++ b/devtools/cypress/integration/item-tracking.e2e.js @@ -6,31 +6,29 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('Tracking items from application to component tree', () => { beforeEach(() => { cy.visit('/'); }); it('should have only one todo item on start', () => { - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo').contains('Buy milk'); }); cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .its('length') .should('eq', 2); }); it('should be able to detect a new todo from user and add it to the tree', () => { - cy.enter('#sample-app') + cy.enterIframe('#sample-app') .then((getBody) => { getBody().find('input.new-todo').type('Buy cookies{enter}'); }) .then(() => { - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo').contains('Buy milk'); getBody().find('app-todo').contains('Build something fun!'); @@ -39,7 +37,7 @@ describe('Tracking items from application to component tree', () => { }); }); - cy.get('.tree-wrapper .tree-node:contains("app-todo[TooltipDirective]")').should( + cy.get('.tree-wrapper ng-tree-node:contains("app-todo[TooltipDirective]")').should( 'have.length', 3, ); diff --git a/devtools/cypress/integration/node-search.e2e.js b/devtools/cypress/integration/node-search.e2e.js index cf6f2802f40..f7ef07b32f6 100644 --- a/devtools/cypress/integration/node-search.e2e.js +++ b/devtools/cypress/integration/node-search.e2e.js @@ -11,7 +11,7 @@ function checkSearchedNodesLength(type, length) { } function inputSearchText(text) { - cy.get('.filter-input').type(text, {force: true}); + cy.get('ng-filter .filter-input').type(text, {force: true}); } function checkComponentName(name) { @@ -19,7 +19,7 @@ function checkComponentName(name) { } function checkEmptyNodes() { - cy.get('.tree-wrapper').find('.matched').should('not.exist'); + cy.get('.tree-wrapper').find('.matched-text').should('not.exist'); } function clickSearchArrows(upwards) { @@ -44,7 +44,7 @@ describe('Search items in component tree', () => { it('should highlight correct nodes when searching and clear out', () => { inputSearchText('todo'); - checkSearchedNodesLength('.matched', 4); + checkSearchedNodesLength('.matched-text', 4); // clear search input inputSearchText('{backspace}{backspace}{backspace}{backspace}'); @@ -53,22 +53,23 @@ describe('Search items in component tree', () => { it('should highlight correct nodes when searching and using arrow keys', () => { inputSearchText('todo'); - checkSearchedNodesLength('.matched', 4); + checkSearchedNodesLength('.matched-text', 4); + checkComponentName('app-todo-demo'); // press down arrow clickSearchArrows(false); checkSearchedNodesLength('.selected', 1); - checkComponentName('app-todo-demo'); + checkComponentName('app-todos'); - // press up arrow + // press down arrow clickSearchArrows(false); checkSearchedNodesLength('.selected', 1); - checkComponentName('app-todos'); + checkComponentName('app-todo'); // press up arrow clickSearchArrows(true); checkSearchedNodesLength('.selected', 1); - checkComponentName('app-todo-demo'); + checkComponentName('app-todos'); // clear search input inputSearchText('{backspace}{backspace}{backspace}{backspace}'); @@ -76,7 +77,7 @@ describe('Search items in component tree', () => { }); it('should select correct node on enter', () => { - inputSearchText('todos{enter}'); + inputSearchText('todo{enter}'); checkSearchedNodesLength('.selected', 1); // should show correct buttons in breadcrumbs @@ -91,7 +92,7 @@ describe('Search items in component tree', () => { checkComponentName('app-todos'); // should display correct title for properties panel - cy.get('ng-property-view-header').should('have.text', 'app-todos'); + cy.get('ng-property-view-header').should('contain.text', 'app-todos'); // should show correct component properties cy.get('ng-property-view').find('mat-tree-node'); diff --git a/devtools/cypress/integration/node-selection.e2e.js b/devtools/cypress/integration/node-selection.e2e.js index 6cdcf3cee13..e3c506e6ac6 100644 --- a/devtools/cypress/integration/node-selection.e2e.js +++ b/devtools/cypress/integration/node-selection.e2e.js @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('node selection', () => { beforeEach(() => { cy.visit('/'); @@ -15,49 +13,55 @@ describe('node selection', () => { describe('logic after change detection', () => { it('should deselect node if it is no longer on the page', () => { - cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist'); + cy.get('.tree-wrapper') + .get('ng-tree-node:contains("app-todo[TooltipDirective]")') + .should('not.have.class', 'selected'); cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); - cy.get('.tree-wrapper').find('.tree-node.selected').its('length').should('eq', 1); + cy.get('.tree-wrapper') + .get('ng-tree-node:contains("app-todo[TooltipDirective]")') + .should('have.class', 'selected'); - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('a:contains("About")').click(); }); - cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist'); + cy.get('.tree-wrapper') + .get('ng-tree-node:contains("app-todo[TooltipDirective]")') + .should('not.exist'); }); it('should reselect the previously selected node if it is still present', () => { - cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist'); + cy.get('.tree-wrapper').get('ng-tree-node.selected').should('not.exist'); - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('input.new-todo').type('Buy cookies{enter}'); }); cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .last() .click({force: true}); - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo:contains("Buy milk")').find('.destroy').click(); }); - cy.get('.tree-wrapper').find('.tree-node.selected').its('length').should('eq', 1); + cy.get('.tree-wrapper').find('ng-tree-node.selected').its('length').should('eq', 1); }); it('should select nodes with same name', () => { cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .last() .click({force: true}); @@ -74,7 +78,7 @@ describe('node selection', () => { describe('breadcrumb logic', () => { it('should overflow when breadcrumb list is long enough', () => { cy.get('.tree-wrapper') - .find('.tree-node:contains("div[TooltipDirective]")') + .find('ng-tree-node:contains("div[TooltipDirective]")') .last() .click({force: true}) .then(() => { @@ -90,7 +94,7 @@ describe('node selection', () => { it('should scroll right when right scroll button is clicked', () => { cy.get('.tree-wrapper') - .find('.tree-node:contains("div[TooltipDirective]")') + .find('ng-tree-node:contains("div[TooltipDirective]")') .last() .click({force: true}) .then(() => { @@ -106,7 +110,7 @@ describe('node selection', () => { cy.get('ng-breadcrumbs') .find('.scroll-button') .last() - .click() + .click({force: true}) .then(() => { expect(scrollLeft()).to.be.greaterThan(0); }); @@ -116,7 +120,7 @@ describe('node selection', () => { it('should scroll left when left scroll button is clicked', () => { cy.get('.tree-wrapper') - .find('.tree-node:contains("div[TooltipDirective]")') + .find('ng-tree-node:contains("div[TooltipDirective]")') .last() .click({force: true}) .then(() => { @@ -132,14 +136,14 @@ describe('node selection', () => { cy.get('ng-breadcrumbs') .find('.scroll-button') .last() - .click() + .click({force: true}) .then(() => { expect(scrollLeft()).to.be.greaterThan(0); cy.get('ng-breadcrumbs') .find('.scroll-button') .first() - .click() + .click({force: true}) .then(() => { expect(scrollLeft()).to.eql(0); }); diff --git a/devtools/cypress/integration/property-edit.e2e.js b/devtools/cypress/integration/property-edit.e2e.js index 363f992c117..b9537db8eff 100644 --- a/devtools/cypress/integration/property-edit.e2e.js +++ b/devtools/cypress/integration/property-edit.e2e.js @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('edit properties of directive in the property view tab', () => { beforeEach(() => { cy.visit('/'); @@ -17,13 +15,13 @@ describe('edit properties of directive in the property view tab', () => { beforeEach(() => { // select todo node in component tree cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); }); it('should be able to enable editMode', () => { - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo input.edit').should('not.be.visible'); }); @@ -36,61 +34,38 @@ describe('edit properties of directive in the property view tab', () => { .type('true') .type('{enter}'); - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('app-todo input.edit').should('be.visible'); }); }); + }); - describe('edit todo property', () => { - beforeEach(() => { - // expand todo state - cy.get('.explorer-panel:contains("app-todo")') - .find('ng-property-view mat-tree-node:contains("todo")') - .click(); + describe('edit title property', () => { + beforeEach(() => { + cy.get('.tree-wrapper') + .find('ng-tree-node:contains("app-todos")') + .first() + .click({force: true}); + }); + + it('should change title in app when edited', () => { + cy.enterIframe('#sample-app').then((getBody) => { + getBody().find('#demo-app-title').contains('Angular Todo'); }); - it('should change todo label in app when edited', () => { - // check initial todo label - cy.enter('#sample-app').then((getBody) => { - getBody().find('app-todo').contains('Buy milk').its('length').should('eq', 1); - }); + // find title variable and run through edit logic + cy.get('.explorer-panel:contains("app-todos")') + .find('ng-property-view mat-tree-node:contains("title")') + .find('ng-property-editor .editor') + .click() + .find('.editor-input') + .clear() + .type('Hello World') + .type('{enter}'); - // find label variable and run through edit logic - cy.get('.explorer-panel:contains("app-todo")') - .find('ng-property-view mat-tree-node:contains("label")') - .find('ng-property-editor .editor') - .click() - .find('.editor-input') - .clear() - .type('Buy cookies') - .type('{enter}'); - - // assert that the page has been updated - cy.enter('#sample-app').then((getBody) => { - getBody().find('app-todo').contains('Buy cookies').its('length').should('eq', 1); - }); - }); - - it('should change todo completed in app when edited', () => { - // check initial todo completed status - cy.enter('#sample-app').then((getBody) => { - getBody().find('app-todo li').not('.completed').its('length').should('eq', 2); - }); - - // find completed variable and run through edit logic - cy.get('.explorer-panel:contains("app-todo")') - .find('ng-property-view mat-tree-node:contains("completed")') - .find('ng-property-editor .editor') - .click() - .find('.editor-input') - .clear() - .type('true') - .type('{enter}'); - - // assert that the page has been updated - cy.enter('#sample-app').then((getBody) => { - getBody().find('app-todo li.completed').its('length').should('eq', 1); - }); + // assert that the page has been updated + cy.enterIframe('#sample-app').then((getBody) => { + getBody().find('#demo-app-title').contains('Hello World'); }); }); }); diff --git a/devtools/cypress/integration/property-update.e2e.js b/devtools/cypress/integration/property-update.e2e.js index 1e9051cd818..bf00d0162f8 100644 --- a/devtools/cypress/integration/property-update.e2e.js +++ b/devtools/cypress/integration/property-update.e2e.js @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -require('cypress-iframe'); - describe('change of the state should reflect in property update', () => { beforeEach(() => { cy.visit('/'); @@ -15,13 +13,13 @@ describe('change of the state should reflect in property update', () => { it('should update the property value', () => { // Complete the todo - cy.enter('#sample-app').then((getBody) => { + cy.enterIframe('#sample-app').then((getBody) => { getBody().find('input[type="checkbox"].toggle').first().click(); }); // Select the todo item cy.get('.tree-wrapper') - .find('.tree-node:contains("app-todo[TooltipDirective]")') + .find('ng-tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); diff --git a/devtools/cypress/integration/view-component-metadata.e2e.js b/devtools/cypress/integration/view-component-metadata.e2e.js index e61c2382f59..25b55dc7ec9 100644 --- a/devtools/cypress/integration/view-component-metadata.e2e.js +++ b/devtools/cypress/integration/view-component-metadata.e2e.js @@ -16,49 +16,49 @@ describe('Viewing component metadata', () => { cy.visit('/'); }); - describe('viewing TodoComponent', () => { + describe('viewing TodosComponent', () => { beforeEach(() => - prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-todo[TooltipDirective]")'), + prepareHeaderExpansionPanelForAssertions('ng-tree-node:contains("app-todos")'), ); it('should display view encapsulation', () => { - cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: Emulated'); + cy.contains('.meta-data-container', 'View Encapsulation: None'); }); it('should display change detection strategy', () => { - cy.contains('.meta-data-container .mat-button:last', 'Change Detection Strategy: OnPush'); + cy.contains('.meta-data-container', 'Change Detection Strategy: Default'); }); }); describe('viewing DemoAppComponent', () => { beforeEach(() => - prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-demo-component")'), + prepareHeaderExpansionPanelForAssertions('ng-tree-node:contains("app-demo-component")'), ); it('should display view encapsulation', () => { - cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: None'); + cy.contains('.meta-data-container', 'View Encapsulation: None'); }); it('should display change detection strategy', () => { - cy.contains('.meta-data-container .mat-button:last', 'Change Detection Strategy: Default'); + cy.contains('.meta-data-container', 'Change Detection Strategy: Default'); }); it('should display correct set of inputs', () => { - cy.contains('.cy-inputs', 'Inputs'); - cy.contains('.cy-inputs mat-tree-node:first span:first', 'inputOne'); - cy.contains('.cy-inputs mat-tree-node:last span:first', 'inputTwo'); + cy.contains('.mat-accordion-content#Inputs', 'Inputs'); + cy.contains('.mat-accordion-content#Inputs mat-tree-node:first span:first', 'inputOne'); + cy.contains('.mat-accordion-content#Inputs mat-tree-node:last span:first', 'inputTwo'); }); it('should display correct set of outputs', () => { - cy.contains('.cy-outputs', 'Outputs'); - cy.contains('.cy-outputs mat-tree-node:first span:first', 'outputOne'); - cy.contains('.cy-outputs mat-tree-node:last span:first', 'outputTwo'); + cy.contains('.mat-accordion-content#Outputs', 'Outputs'); + cy.contains('.mat-accordion-content#Outputs mat-tree-node:first span:first', 'outputOne'); + cy.contains('.mat-accordion-content#Outputs mat-tree-node:last span:first', 'outputTwo'); }); it('should display correct set of properties', () => { - cy.contains('.cy-properties', 'Properties'); - cy.contains('.cy-properties mat-tree-node:first span:first', 'elementRef'); - cy.contains('.cy-properties mat-tree-node:last span:first', 'zippy'); + cy.contains('.mat-accordion-content#Properties', 'Properties'); + cy.contains('.mat-accordion-content#Properties mat-tree-node:first span:first', 'elementRef'); + cy.contains('.mat-accordion-content#Properties mat-tree-node:last span:first', 'zippy'); }); }); }); diff --git a/devtools/cypress/support/commands.js b/devtools/cypress/support/commands.js index 4ba9df19a44..e144a5c5dab 100644 --- a/devtools/cypress/support/commands.js +++ b/devtools/cypress/support/commands.js @@ -6,28 +6,25 @@ * found in the LICENSE file at https://angular.dev/license */ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +/** + * Selects an Iframe and returns its body when it's done loading. + * @param {string} selector - The selector for the iframe. + * @returns {Function} A function that returns the wrapped body of the iframe. + */ +function enterIframe(selector) { + return cy.get(selector, {log: false}).then({timeout: 30000}, async (frame) => { + const contentWindow = frame.prop('contentWindow'); + + while ( + contentWindow.location.toString() === 'about:blank' || + contentWindow.document.readyState !== 'complete' + ) { + await new Promise((resolve) => setTimeout(resolve)); + } + + // return the body of the iframe wrapped in cypress + return () => cy.wrap(contentWindow.document.body); + }); +} + +Cypress.Commands.add('enterIframe', enterIframe); diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html index 4ca38e274f3..818af18b262 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab/property-view/property-view-body.component.html @@ -23,7 +23,7 @@ } @for (panel of panels(); track $index) { -
+
@if (panel.controls().dataSource.data.length > 0) { diff --git a/devtools/src/app/demo-app/demo-app.component.html b/devtools/src/app/demo-app/demo-app.component.html index 9022b548952..6499a50d759 100644 --- a/devtools/src/app/demo-app/demo-app.component.html +++ b/devtools/src/app/demo-app/demo-app.component.html @@ -2,4 +2,5 @@ This is my content
HTMLElement
+
Test Structural Directive
diff --git a/devtools/src/app/demo-app/demo-app.component.ts b/devtools/src/app/demo-app/demo-app.component.ts index 19164b6693c..0f80c8e399c 100644 --- a/devtools/src/app/demo-app/demo-app.component.ts +++ b/devtools/src/app/demo-app/demo-app.component.ts @@ -10,11 +10,15 @@ import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, + Directive, ElementRef, + inject, input, output, signal, + TemplateRef, viewChild, + ViewContainerRef, ViewEncapsulation, } from '@angular/core'; @@ -23,13 +27,30 @@ import {HeavyComponent} from './heavy.component'; import {SamplePropertiesComponent} from './sample-properties.component'; import {RouterOutlet} from '@angular/router'; +// structual directive example +@Directive({ + selector: '[appStructural]', + host: { + '[class.app-structural]': 'true', + }, +}) +export class StructuralDirective { + templateRef = inject(TemplateRef); + viewContainerRef = inject(ViewContainerRef); + + ngOnInit() { + // Example of using the structural directive + this.viewContainerRef.createEmbeddedView(this.templateRef); + } +} + @Component({ selector: 'app-demo-component', templateUrl: './demo-app.component.html', styleUrls: ['./demo-app.component.scss'], encapsulation: ViewEncapsulation.None, schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [HeavyComponent, SamplePropertiesComponent, RouterOutlet], + imports: [StructuralDirective, HeavyComponent, SamplePropertiesComponent, RouterOutlet], }) export class DemoAppComponent { readonly zippy = viewChild(ZippyComponent); diff --git a/devtools/src/app/demo-app/todo/home/todos.component.html b/devtools/src/app/demo-app/todo/home/todos.component.html index d972095ea5b..5917c5c1031 100644 --- a/devtools/src/app/demo-app/todo/home/todos.component.html +++ b/devtools/src/app/demo-app/todo/home/todos.component.html @@ -4,7 +4,7 @@

{{ 'Sample text processed by a pipe' | sample }}

-

todos

+

{{title}}

{ imports: [RouterLink, TodoComponent, TooltipDirective, SamplePipe, TodosFilter], }) export class TodosComponent implements OnInit, OnDestroy { + title = 'Angular Todo'; + todos: Todo[] = [ { label: 'Buy milk',