diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 085b4c91d9c..4f93e99efaf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,15 +56,6 @@ 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 e70a6481709..b07bbc9eb10 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -60,15 +60,6 @@ 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 deleted file mode 100644 index 706dc62d9fb..00000000000 --- a/devtools/cypress/cypress.config.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @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 c70b5610d45..714f7bf4f88 100644 --- a/devtools/cypress/integration/base.e2e.js +++ b/devtools/cypress/integration/base.e2e.js @@ -6,13 +6,15 @@ * 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.enterIframe('#sample-app').then((getBody) => { + cy.enter('#sample-app').then((getBody) => { getBody().contains('Todos'); getBody().contains('About'); getBody().contains('Clear completed'); @@ -21,15 +23,15 @@ describe('Testing the Todo app Demo', () => { }); it('should contain the "Components" tab', () => { - cy.contains('.devtools-nav', 'Components'); + cy.contains('.mat-tab-links', 'Components'); }); it('should contain the "Profiler" tab', () => { - cy.contains('.devtools-nav', 'Profiler'); + cy.contains('.mat-tab-links', 'Profiler'); }); it('should contain "app-root" and "app-todo-demo" in the component tree', () => { - cy.contains('ng-tree-node', 'app-root'); - cy.contains('ng-tree-node', 'app-todo-demo'); + cy.contains('.tree-node', 'app-root'); + cy.contains('.tree-node', 'app-todo-demo'); }); }); diff --git a/devtools/cypress/integration/comment-nodes.e2e.js b/devtools/cypress/integration/comment-nodes.e2e.js index fff56712e91..492118e1766 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('.cdk-overlay-container mat-slide-toggle label:contains("Show comment nodes")').click(); + cy.get('#mat-slide-toggle-3 > label > div').click(); } describe('Comment nodes', () => { @@ -17,14 +17,14 @@ describe('Comment nodes', () => { }); it('should not find any comment nodes by default', () => { - const nodes = cy.$$('ng-tree-node:contains("#comment")'); + const nodes = cy.$$('.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('ng-tree-node:contains("#comment")') + .find('.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 711200894f2..66d05401b33 100644 --- a/devtools/cypress/integration/elements.e2e.js +++ b/devtools/cypress/integration/elements.e2e.js @@ -12,9 +12,6 @@ describe('Angular Elements', () => { }); it('should recognize the zippy as an Angular Element', () => { - cy.get('.tree-wrapper') - .find('ng-tree-node:contains("app-zippy")') - .its('length') - .should('eq', 1); + cy.get('.tree-wrapper').find('.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 36aca2fc639..99146a9d45b 100644 --- a/devtools/cypress/integration/item-tracking.e2e.js +++ b/devtools/cypress/integration/item-tracking.e2e.js @@ -6,29 +6,31 @@ * 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.enterIframe('#sample-app').then((getBody) => { + cy.enter('#sample-app').then((getBody) => { getBody().find('app-todo').contains('Buy milk'); }); cy.get('.tree-wrapper') - .find('ng-tree-node:contains("app-todo[TooltipDirective]")') + .find('.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.enterIframe('#sample-app') + cy.enter('#sample-app') .then((getBody) => { getBody().find('input.new-todo').type('Buy cookies{enter}'); }) .then(() => { - cy.enterIframe('#sample-app').then((getBody) => { + cy.enter('#sample-app').then((getBody) => { getBody().find('app-todo').contains('Buy milk'); getBody().find('app-todo').contains('Build something fun!'); @@ -37,7 +39,7 @@ describe('Tracking items from application to component tree', () => { }); }); - cy.get('.tree-wrapper ng-tree-node:contains("app-todo[TooltipDirective]")').should( + cy.get('.tree-wrapper .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 f7ef07b32f6..cf6f2802f40 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('ng-filter .filter-input').type(text, {force: true}); + cy.get('.filter-input').type(text, {force: true}); } function checkComponentName(name) { @@ -19,7 +19,7 @@ function checkComponentName(name) { } function checkEmptyNodes() { - cy.get('.tree-wrapper').find('.matched-text').should('not.exist'); + cy.get('.tree-wrapper').find('.matched').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-text', 4); + checkSearchedNodesLength('.matched', 4); // clear search input inputSearchText('{backspace}{backspace}{backspace}{backspace}'); @@ -53,23 +53,22 @@ describe('Search items in component tree', () => { it('should highlight correct nodes when searching and using arrow keys', () => { inputSearchText('todo'); - checkSearchedNodesLength('.matched-text', 4); - checkComponentName('app-todo-demo'); + checkSearchedNodesLength('.matched', 4); // press down arrow clickSearchArrows(false); checkSearchedNodesLength('.selected', 1); + checkComponentName('app-todo-demo'); + + // press up arrow + clickSearchArrows(false); + checkSearchedNodesLength('.selected', 1); checkComponentName('app-todos'); - // press down arrow - clickSearchArrows(false); - checkSearchedNodesLength('.selected', 1); - checkComponentName('app-todo'); - // press up arrow clickSearchArrows(true); checkSearchedNodesLength('.selected', 1); - checkComponentName('app-todos'); + checkComponentName('app-todo-demo'); // clear search input inputSearchText('{backspace}{backspace}{backspace}{backspace}'); @@ -77,7 +76,7 @@ describe('Search items in component tree', () => { }); it('should select correct node on enter', () => { - inputSearchText('todo{enter}'); + inputSearchText('todos{enter}'); checkSearchedNodesLength('.selected', 1); // should show correct buttons in breadcrumbs @@ -92,7 +91,7 @@ describe('Search items in component tree', () => { checkComponentName('app-todos'); // should display correct title for properties panel - cy.get('ng-property-view-header').should('contain.text', 'app-todos'); + cy.get('ng-property-view-header').should('have.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 e3c506e6ac6..6cdcf3cee13 100644 --- a/devtools/cypress/integration/node-selection.e2e.js +++ b/devtools/cypress/integration/node-selection.e2e.js @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ +require('cypress-iframe'); + describe('node selection', () => { beforeEach(() => { cy.visit('/'); @@ -13,55 +15,49 @@ 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('ng-tree-node:contains("app-todo[TooltipDirective]")') - .should('not.have.class', 'selected'); + cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist'); cy.get('.tree-wrapper') - .find('ng-tree-node:contains("app-todo[TooltipDirective]")') + .find('.tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); - cy.get('.tree-wrapper') - .get('ng-tree-node:contains("app-todo[TooltipDirective]")') - .should('have.class', 'selected'); + cy.get('.tree-wrapper').find('.tree-node.selected').its('length').should('eq', 1); - cy.enterIframe('#sample-app').then((getBody) => { + cy.enter('#sample-app').then((getBody) => { getBody().find('a:contains("About")').click(); }); - cy.get('.tree-wrapper') - .get('ng-tree-node:contains("app-todo[TooltipDirective]")') - .should('not.exist'); + cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist'); }); it('should reselect the previously selected node if it is still present', () => { - cy.get('.tree-wrapper').get('ng-tree-node.selected').should('not.exist'); + cy.get('.tree-wrapper').get('.tree-node.selected').should('not.exist'); - cy.enterIframe('#sample-app').then((getBody) => { + cy.enter('#sample-app').then((getBody) => { getBody().find('input.new-todo').type('Buy cookies{enter}'); }); cy.get('.tree-wrapper') - .find('ng-tree-node:contains("app-todo[TooltipDirective]")') + .find('.tree-node:contains("app-todo[TooltipDirective]")') .last() .click({force: true}); - cy.enterIframe('#sample-app').then((getBody) => { + cy.enter('#sample-app').then((getBody) => { getBody().find('app-todo:contains("Buy milk")').find('.destroy').click(); }); - cy.get('.tree-wrapper').find('ng-tree-node.selected').its('length').should('eq', 1); + cy.get('.tree-wrapper').find('.tree-node.selected').its('length').should('eq', 1); }); it('should select nodes with same name', () => { cy.get('.tree-wrapper') - .find('ng-tree-node:contains("app-todo[TooltipDirective]")') + .find('.tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); cy.get('.tree-wrapper') - .find('ng-tree-node:contains("app-todo[TooltipDirective]")') + .find('.tree-node:contains("app-todo[TooltipDirective]")') .last() .click({force: true}); @@ -78,7 +74,7 @@ describe('node selection', () => { describe('breadcrumb logic', () => { it('should overflow when breadcrumb list is long enough', () => { cy.get('.tree-wrapper') - .find('ng-tree-node:contains("div[TooltipDirective]")') + .find('.tree-node:contains("div[TooltipDirective]")') .last() .click({force: true}) .then(() => { @@ -94,7 +90,7 @@ describe('node selection', () => { it('should scroll right when right scroll button is clicked', () => { cy.get('.tree-wrapper') - .find('ng-tree-node:contains("div[TooltipDirective]")') + .find('.tree-node:contains("div[TooltipDirective]")') .last() .click({force: true}) .then(() => { @@ -110,7 +106,7 @@ describe('node selection', () => { cy.get('ng-breadcrumbs') .find('.scroll-button') .last() - .click({force: true}) + .click() .then(() => { expect(scrollLeft()).to.be.greaterThan(0); }); @@ -120,7 +116,7 @@ describe('node selection', () => { it('should scroll left when left scroll button is clicked', () => { cy.get('.tree-wrapper') - .find('ng-tree-node:contains("div[TooltipDirective]")') + .find('.tree-node:contains("div[TooltipDirective]")') .last() .click({force: true}) .then(() => { @@ -136,14 +132,14 @@ describe('node selection', () => { cy.get('ng-breadcrumbs') .find('.scroll-button') .last() - .click({force: true}) + .click() .then(() => { expect(scrollLeft()).to.be.greaterThan(0); cy.get('ng-breadcrumbs') .find('.scroll-button') .first() - .click({force: true}) + .click() .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 b9537db8eff..363f992c117 100644 --- a/devtools/cypress/integration/property-edit.e2e.js +++ b/devtools/cypress/integration/property-edit.e2e.js @@ -6,6 +6,8 @@ * 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('/'); @@ -15,13 +17,13 @@ describe('edit properties of directive in the property view tab', () => { beforeEach(() => { // select todo node in component tree cy.get('.tree-wrapper') - .find('ng-tree-node:contains("app-todo[TooltipDirective]")') + .find('.tree-node:contains("app-todo[TooltipDirective]")') .first() .click({force: true}); }); it('should be able to enable editMode', () => { - cy.enterIframe('#sample-app').then((getBody) => { + cy.enter('#sample-app').then((getBody) => { getBody().find('app-todo input.edit').should('not.be.visible'); }); @@ -34,38 +36,61 @@ describe('edit properties of directive in the property view tab', () => { .type('true') .type('{enter}'); - cy.enterIframe('#sample-app').then((getBody) => { + cy.enter('#sample-app').then((getBody) => { getBody().find('app-todo input.edit').should('be.visible'); }); }); - }); - 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'); + 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(); }); - // 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}'); + 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); + }); - // assert that the page has been updated - cy.enterIframe('#sample-app').then((getBody) => { - getBody().find('#demo-app-title').contains('Hello World'); + // 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); + }); }); }); }); diff --git a/devtools/cypress/integration/property-update.e2e.js b/devtools/cypress/integration/property-update.e2e.js index bf00d0162f8..1e9051cd818 100644 --- a/devtools/cypress/integration/property-update.e2e.js +++ b/devtools/cypress/integration/property-update.e2e.js @@ -6,6 +6,8 @@ * 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('/'); @@ -13,13 +15,13 @@ describe('change of the state should reflect in property update', () => { it('should update the property value', () => { // Complete the todo - cy.enterIframe('#sample-app').then((getBody) => { + cy.enter('#sample-app').then((getBody) => { getBody().find('input[type="checkbox"].toggle').first().click(); }); // Select the todo item cy.get('.tree-wrapper') - .find('ng-tree-node:contains("app-todo[TooltipDirective]")') + .find('.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 25b55dc7ec9..e61c2382f59 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 TodosComponent', () => { + describe('viewing TodoComponent', () => { beforeEach(() => - prepareHeaderExpansionPanelForAssertions('ng-tree-node:contains("app-todos")'), + prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-todo[TooltipDirective]")'), ); it('should display view encapsulation', () => { - cy.contains('.meta-data-container', 'View Encapsulation: None'); + cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: Emulated'); }); it('should display change detection strategy', () => { - cy.contains('.meta-data-container', 'Change Detection Strategy: Default'); + cy.contains('.meta-data-container .mat-button:last', 'Change Detection Strategy: OnPush'); }); }); describe('viewing DemoAppComponent', () => { beforeEach(() => - prepareHeaderExpansionPanelForAssertions('ng-tree-node:contains("app-demo-component")'), + prepareHeaderExpansionPanelForAssertions('.tree-node:contains("app-demo-component")'), ); it('should display view encapsulation', () => { - cy.contains('.meta-data-container', 'View Encapsulation: None'); + cy.contains('.meta-data-container .mat-button:first', 'View Encapsulation: None'); }); it('should display change detection strategy', () => { - cy.contains('.meta-data-container', 'Change Detection Strategy: Default'); + cy.contains('.meta-data-container .mat-button:last', 'Change Detection Strategy: Default'); }); it('should display correct set of inputs', () => { - 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'); + 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'); }); it('should display correct set of outputs', () => { - 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'); + 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'); }); it('should display correct set of properties', () => { - 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'); + 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'); }); }); }); diff --git a/devtools/cypress/support/commands.js b/devtools/cypress/support/commands.js index e144a5c5dab..4ba9df19a44 100644 --- a/devtools/cypress/support/commands.js +++ b/devtools/cypress/support/commands.js @@ -6,25 +6,28 @@ * found in the LICENSE file at https://angular.dev/license */ -/** - * 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); +// *********************************************** +// 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) => { ... }) 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 818af18b262..4ca38e274f3 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) { -
{{ 'Sample text processed by a pipe' | sample }}