From cceacc8493ed1d3b3465c133eea72f57dec3d414 Mon Sep 17 00:00:00 2001 From: splincode Date: Tue, 12 Nov 2024 11:53:04 +0300 Subject: [PATCH] fix: the value in the component is incorrectly updated when it is passed as an empty string using formControl --- .cspell.json | 2 +- .firebaserc | 15 ++ .github/workflows/deploy-preview.yml | 1 + .github/workflows/e2e.yml | 11 ++ firebase.json | 35 +++-- package-lock.json | 131 ++++++++++++++++++ package.json | 3 +- projects/demo-playwright/playwright.config.ts | 14 +- projects/demo-playwright/project.json | 2 +- projects/demo-playwright/tests/reset.spec.ts | 23 +++ .../demo-playwright/tests/toolbar.spec.ts | 24 +++- .../components/editor/editor.component.html | 1 - .../components/editor/editor.component.ts | 5 + .../toolbar-tools/text-color/index.ts | 12 +- .../tiptap-editor/tiptap-editor.directive.ts | 14 +- 15 files changed, 262 insertions(+), 31 deletions(-) create mode 100644 .firebaserc create mode 100644 projects/demo-playwright/tests/reset.spec.ts diff --git a/.cspell.json b/.cspell.json index 9f34d7e8e..0c3fdb9cd 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json", "import": ["@taiga-ui/cspell-config/cspell.config.js"], - "ignoreWords": ["todoify", "Lyyy", "Textblock", "hardbreak", "softbreak", "inlines"], + "ignoreWords": ["todoify", "Lyyy", "Textblock", "hardbreak", "softbreak", "inlines", "estruyf"], "files": ["*/*.*"] } diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 000000000..74fd21655 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,15 @@ +{ + "projects": {}, + "targets": { + "taiga-editor": { + "hosting": { + "taiga-editor-demo": [ + "taiga-editor-demo" + ], + "taiga-editor-e2e-report": [ + "taiga-editor-e2e-report" + ] + } + } + } +} diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 879c1ac06..0da28959f 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -18,6 +18,7 @@ jobs: repoToken: ${{ secrets.GITHUB_TOKEN }} firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_TUI_EDITOR }} projectId: taiga-editor + target: taiga-editor-demo expires: 1d concurrency: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6faee88c5..945a7d2f3 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -44,6 +44,17 @@ jobs: compression-level: 0 retention-days: 1 + - name: Deploy report + uses: FirebaseExtended/action-hosting-deploy@v0 + continue-on-error: true + if: ${{ env.IS_FORK == 'false' && env.IS_DEPENDABOT == 'false' }} + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_TUI_EDITOR }} + projectId: taiga-editor + target: taiga-editor-e2e-report + expires: 1d + - id: diff-checker run: | echo "diff_exist=$(find ./projects/demo-playwright/tests-results -regex '.*diff\.png$' | wc -l | sed -e 's/^[[:space:]]*//')" >> $GITHUB_OUTPUT diff --git a/firebase.json b/firebase.json index 86e0ea473..240f60010 100644 --- a/firebase.json +++ b/firebase.json @@ -1,12 +1,27 @@ { - "hosting": { - "public": "dist/demo/browser", - "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - } + "$schema": "https://raw.githubusercontent.com/firebase/firebase-tools/master/schema/firebase-config.json", + "hosting": [ + { + "target": "taiga-editor-demo", + "public": "dist/demo/browser", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + }, + { + "target": "taiga-editor-e2e-report", + "public": "projects/demo-playwright/tests-report", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } + ] } diff --git a/package-lock.json b/package-lock.json index 0b9084958..6e416f10e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@angular/platform-browser-dynamic": "16.2.12", "@angular/platform-server": "16.2.12", "@angular/router": "16.2.12", + "@estruyf/github-actions-reporter": "1.9.2", "@ng-web-apis/common": "4.11.1", "@ng-web-apis/universal": "4.11.1", "@nguniversal/builders": "16.2.0", @@ -87,6 +88,45 @@ "webpack": "5.96.1" } }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@adobe/css-tools": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", @@ -5216,6 +5256,48 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@estruyf/github-actions-reporter": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@estruyf/github-actions-reporter/-/github-actions-reporter-1.9.2.tgz", + "integrity": "sha512-D2+ePwTjbd4siY7VIWX1dJrE1bU3x9OO+FOj99hqU4zQhVGwMm0ozgNDUzJgHxdRZKtFoGcYWV5YLfZettR38A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.0", + "ansi-to-html": "^0.7.2", + "marked": "^12.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/estruyf" + }, + "peerDependencies": { + "@playwright/test": "^1.42.1" + } + }, + "node_modules/@estruyf/github-actions-reporter/node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -13880,6 +13962,32 @@ "node": ">=4" } }, + "node_modules/ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^2.2.0" + }, + "bin": { + "ansi-to-html": "bin/ansi-to-html" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ansi-to-html/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -39986,6 +40094,16 @@ "dev": true, "license": "ISC" }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -40223,6 +40341,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index cb1fc9737..de48a68e1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "prepare": "husky", "start": "nx serve editor-demo", "lint": "eslint .", - "cspell": "cspell --relative --dot --gitignore .", + "cspell": "cspell --relative --dot --no-progress --gitignore .", "prettier": "prettier !package-lock.json !taiga-ui/** . --ignore-path .gitignore", "stylelint": "stylelint './projects/**/*.{less,css}' --config package.json", "release": "npx syncer && npx nx publish editor" @@ -80,6 +80,7 @@ "@angular/platform-browser-dynamic": "16.2.12", "@angular/platform-server": "16.2.12", "@angular/router": "16.2.12", + "@estruyf/github-actions-reporter": "1.9.2", "@ng-web-apis/common": "4.11.1", "@ng-web-apis/universal": "4.11.1", "@nguniversal/builders": "16.2.0", diff --git a/projects/demo-playwright/playwright.config.ts b/projects/demo-playwright/playwright.config.ts index bafd4f92e..df4ec46dc 100644 --- a/projects/demo-playwright/playwright.config.ts +++ b/projects/demo-playwright/playwright.config.ts @@ -8,7 +8,19 @@ export default defineConfig({ testMatch: '**/*.spec.ts', outputDir: 'tests-results', snapshotDir: 'snapshots', - reporter: process.env.CI ? 'github' : [['html', {outputFolder: 'tests-report'}]], + reporter: process.env.CI + ? [ + ['html', {outputFolder: 'tests-report'}], + [ + '@estruyf/github-actions-reporter', + { + title: 'My custom title', + useDetails: true, + showError: true, + }, + ], + ] + : [['html', {outputFolder: 'tests-report'}]], fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, diff --git a/projects/demo-playwright/project.json b/projects/demo-playwright/project.json index a1035aa2e..04bc4c11a 100644 --- a/projects/demo-playwright/project.json +++ b/projects/demo-playwright/project.json @@ -14,7 +14,7 @@ "e2e-ui": { "executor": "nx:run-commands", "options": { - "command": "nx e2e editor-demo-playwright -- --ui --debug --update-snapshots" + "command": "nx e2e editor-demo-playwright --ui --debug --update-snapshots" } } } diff --git a/projects/demo-playwright/tests/reset.spec.ts b/projects/demo-playwright/tests/reset.spec.ts new file mode 100644 index 000000000..0ac92d85f --- /dev/null +++ b/projects/demo-playwright/tests/reset.spec.ts @@ -0,0 +1,23 @@ +import {expect, test} from '@playwright/test'; + +import {tuiGoto} from '../utils'; + +test.describe('Reset', () => { + test('Correct reset value from wysiwyg', async ({page}) => { + await tuiGoto(page, '/starter-kit?placeholder=Hello'); + + await expect(page.locator('tui-editor')).toHaveScreenshot('Reset-01.png'); + + await page.locator('button:has-text("Reset")').click(); + + await expect(page.locator('tui-editor')).toHaveScreenshot('Reset-02.png'); + + await page.locator('tui-editor [contenteditable]').fill('12345'); + + await expect(page.locator('tui-editor')).toHaveScreenshot('Reset-03.png'); + + await page.locator('button:has-text("Reset")').click(); + + await expect(page.locator('tui-editor')).toHaveScreenshot('Reset-04.png'); + }); +}); diff --git a/projects/demo-playwright/tests/toolbar.spec.ts b/projects/demo-playwright/tests/toolbar.spec.ts index e0f419a9b..0636306fa 100644 --- a/projects/demo-playwright/tests/toolbar.spec.ts +++ b/projects/demo-playwright/tests/toolbar.spec.ts @@ -15,9 +15,11 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.locator('[automation-id="toolbar__color-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await page.locator('[automation-id="toolbar__hilite-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await expect(page.locator('#demo-content tui-editor')).toHaveScreenshot( 'Toolbar-02.png', @@ -30,6 +32,7 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.locator('[automation-id="toolbar__color-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await expect(page.locator('#demo-content tui-editor')).toHaveScreenshot( 'Toolbar-03.png', @@ -105,28 +108,32 @@ test.describe('Toolbar', () => { await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-13.png'); await page.locator('[automation-id="toolbar__ordering-list-button"]').focus(); - await page.waitForTimeout(100); + await page.waitForTimeout(200); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await page .locator('[automation-id="toolbar__un-ordered-list-button"].t-option') .focus(); - await page.waitForTimeout(100); + await page.waitForTimeout(200); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await page.locator('[automation-id="toolbar__font-style-button"]').focus(); - await page.waitForTimeout(100); + await page.waitForTimeout(200); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await page.locator('[contenteditable]').nth(0).focus(); - await page.waitForTimeout(100); + await page.waitForTimeout(200); await page.keyboard.type('12345'); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-14.png'); await page.locator('[automation-id="toolbar__insert-table-button"]').focus(); - await page.waitForTimeout(100); + await page.waitForTimeout(200); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); const cell = page .locator('tui-table-size-selector .t-column') @@ -134,7 +141,7 @@ test.describe('Toolbar', () => { .locator('.t-cell') .nth(1); - await page.waitForTimeout(100); + await page.waitForTimeout(200); await cell.hover(); await cell.click(); @@ -149,17 +156,20 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.locator('[automation-id="toolbar__align-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-16.png'); await page.keyboard.press('ArrowRight'); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-17.png'); await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowLeft'); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-18.png'); }); @@ -177,6 +187,7 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.locator('[automation-id="toolbar__undo-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await page.waitForTimeout(300); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-20.png'); @@ -184,6 +195,7 @@ test.describe('Toolbar', () => { await page.locator('[contenteditable]').nth(0).focus(); await page.locator('[automation-id="toolbar__redo-button"]').focus(); await page.keyboard.press('Enter'); + await page.waitForTimeout(200); await page.waitForTimeout(300); await expect(page.locator('tui-editor')).toHaveScreenshot('Toolbar-21.png'); diff --git a/projects/editor/components/editor/editor.component.html b/projects/editor/components/editor/editor.component.html index 26e1980c6..d9cf71d7e 100644 --- a/projects/editor/components/editor/editor.component.html +++ b/projects/editor/components/editor/editor.component.html @@ -56,7 +56,6 @@ tuiTiptapEditor class="tui-editor-socket" [editable]="interactive()" - [value]="firstInitialValue" (valueChange)="onModelChange($event)" > diff --git a/projects/editor/components/editor/editor.component.ts b/projects/editor/components/editor/editor.component.ts index bb99b0cb4..14f8b3ee2 100644 --- a/projects/editor/components/editor/editor.component.ts +++ b/projects/editor/components/editor/editor.component.ts @@ -137,6 +137,7 @@ export class TuiEditor extends TuiControl implements OnDestroy { this.patchContentEditableElement(); this.listenResizeEvents(); + this.editorService.setValue(this.firstInitialValue); this.editorLoaded.set(true); }); @@ -235,6 +236,10 @@ export class TuiEditor extends TuiControl implements OnDestroy { if (!this.focused()) { this.doc?.getSelection?.()?.removeAllRanges(); } + + if (this.editorLoaded()) { + this.editorService.setValue(processed ?? ''); + } } public ngOnDestroy(): void { diff --git a/projects/editor/components/toolbar-tools/text-color/index.ts b/projects/editor/components/toolbar-tools/text-color/index.ts index 6f56a8206..9ddd5e0e9 100644 --- a/projects/editor/components/toolbar-tools/text-color/index.ts +++ b/projects/editor/components/toolbar-tools/text-color/index.ts @@ -3,7 +3,11 @@ import type {OnInit} from '@angular/core'; import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; import {TuiButton, TuiDropdown, TuiHint} from '@taiga-ui/core'; import type {AbstractTuiEditor} from '@taiga-ui/editor/common'; -import {TUI_EDITOR_OPTIONS, TUI_EDITOR_TOOLBAR_TEXTS} from '@taiga-ui/editor/common'; +import { + EDITOR_BLANK_COLOR, + TUI_EDITOR_OPTIONS, + TUI_EDITOR_TOOLBAR_TEXTS, +} from '@taiga-ui/editor/common'; import {TuiTiptapEditorService} from '@taiga-ui/editor/directives'; import {TuiPaletteModule} from '@taiga-ui/legacy'; import type {Observable} from 'rxjs'; @@ -45,7 +49,11 @@ export class TuiTextColor implements OnInit { private initStream(): void { this.fontColor$ = this.editor?.stateChange$.pipe( - map(() => this.editor?.getFontColor() ?? this.options.blankColor), + map(() => { + const color = this.editor?.getFontColor() ?? this.options.blankColor; + + return color !== EDITOR_BLANK_COLOR ? color : ''; + }), distinctUntilChanged(), ) ?? null; } diff --git a/projects/editor/directives/tiptap-editor/tiptap-editor.directive.ts b/projects/editor/directives/tiptap-editor/tiptap-editor.directive.ts index 90b2d6091..e33fa9896 100644 --- a/projects/editor/directives/tiptap-editor/tiptap-editor.directive.ts +++ b/projects/editor/directives/tiptap-editor/tiptap-editor.directive.ts @@ -15,20 +15,18 @@ export class TuiTiptapEditor { protected editorContainer = inject(INITIALIZATION_TIPTAP_CONTAINER); + protected readonly $ = inject(TIPTAP_EDITOR) + .pipe(takeUntilDestroyed()) + .subscribe(() => + this.renderer.appendChild(this.el.nativeElement, this.editorContainer), + ); + @Output() public readonly valueChange = this.editor.valueChange$; @Output() public readonly stateChange = this.editor.stateChange$; - constructor() { - inject(TIPTAP_EDITOR) - .pipe(takeUntilDestroyed()) - .subscribe(() => - this.renderer.appendChild(this.el.nativeElement, this.editorContainer), - ); - } - @Input() public set value(value: string) { this.editor.setValue(value);