From 6457c979f5dd77b5b66e90a48d2c0e0321326ff5 Mon Sep 17 00:00:00 2001 From: doodlewind <7312949+doodlewind@users.noreply.github.com> Date: Mon, 14 Apr 2025 07:51:56 +0000 Subject: [PATCH 1/4] test(editor): add tests for turbo renderer state machine (#11659) --- .../gfx/turbo-renderer/src/turbo-renderer.ts | 4 ++ blocksuite/integration-test/README.md | 27 +++++++++ .../edgeless/viewport-renderer.spec.ts | 55 ++++++++++++++++++- .../src/__tests__/utils/setup.ts | 25 ++++++--- 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/blocksuite/affine/gfx/turbo-renderer/src/turbo-renderer.ts b/blocksuite/affine/gfx/turbo-renderer/src/turbo-renderer.ts index ddf652a47a..b92c405d79 100644 --- a/blocksuite/affine/gfx/turbo-renderer/src/turbo-renderer.ts +++ b/blocksuite/affine/gfx/turbo-renderer/src/turbo-renderer.ts @@ -83,6 +83,10 @@ export class ViewportTurboRendererExtension extends GfxExtension { private viewportElement: GfxViewportElement | null = null; private readonly refresh$ = new Subject(); + public get currentState(): RenderingState { + return this.state$.value; + } + constructor(gfx: GfxController) { super(gfx); diff --git a/blocksuite/integration-test/README.md b/blocksuite/integration-test/README.md index 2412e12fd9..cc84a51cd8 100644 --- a/blocksuite/integration-test/README.md +++ b/blocksuite/integration-test/README.md @@ -1,3 +1,30 @@ # `@blocksuite/integration-test` Integration test for BlockSuite. + +## Running Tests + +You can run all integration tests using: + +```bash +cd blocksuite/integration-test +yarn test:unit +``` + +To run a specific test or test file, use the `-t` flag with a test name pattern: + +```bash +# Run a specific test +yarn test:unit -t "should access turbo renderer instance" + +# Run all tests in a specific file +yarn test:unit src/__tests__/edgeless/viewport-renderer.spec.ts +``` + +For debugging tests with the Playwright debugger: + +```bash +yarn test:debug + +yarn test:debug -t "should access turbo renderer instance" +``` diff --git a/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts b/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts index 52613603b6..fd218e24cf 100644 --- a/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts +++ b/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts @@ -1,3 +1,6 @@ +/** + * Please refer to integration-test/README.md for commands to run tests. + */ import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph'; import { TurboRendererConfigFactory, @@ -7,7 +10,11 @@ import { beforeEach, describe, expect, test } from 'vitest'; import { wait } from '../utils/common.js'; import { addSampleNotes } from '../utils/doc-generator.js'; -import { createPainterWorker, setupEditor } from '../utils/setup.js'; +import { + createPainterWorker, + getRenderer, + setupEditor, +} from '../utils/setup.js'; describe('viewport turbo renderer', () => { beforeEach(async () => { @@ -28,4 +35,50 @@ describe('viewport turbo renderer', () => { const notes = document.querySelectorAll('affine-edgeless-note'); expect(notes.length).toBe(6); }); + + test('should access turbo renderer instance', async () => { + const renderer = getRenderer(); + expect(renderer).toBeDefined(); + expect(renderer instanceof ViewportTurboRendererExtension).toBe(true); + expect(renderer.canvas).toBeInstanceOf(HTMLCanvasElement); + }); + + test('initial state should be pending', async () => { + const renderer = getRenderer(); + expect(renderer.currentState).toBe('pending'); + }); + + test('zooming should change state to zooming', async () => { + const renderer = getRenderer(); + renderer.viewport.zooming$.next(true); + await wait(); + expect(renderer.currentState).toBe('zooming'); + renderer.viewport.zooming$.next(false); + await wait(); + expect(renderer.currentState).not.toBe('zooming'); + }); + + test('state should become ready after rendering', async () => { + addSampleNotes(doc, 1); + await wait(100); + const renderer = getRenderer(); + renderer.viewport.zooming$.next(false); + await wait(renderer.options.debounceTime + 100); + if (renderer.viewport.zoom <= renderer.options.zoomThreshold) { + expect(renderer.currentState).toBe('ready'); + } else { + expect(renderer.currentState).toBe('pending'); + } + }); + + test('invalidation should reset state to pending', async () => { + const renderer = getRenderer(); + addSampleNotes(doc, 1); + expect(renderer.currentState).toBe('pending'); + await wait(renderer.options.debounceTime + 500); + expect(renderer.currentState).toBe('ready'); + addSampleNotes(doc, 1); + await wait(100); + expect(renderer.currentState).toBe('pending'); + }); }); diff --git a/blocksuite/integration-test/src/__tests__/utils/setup.ts b/blocksuite/integration-test/src/__tests__/utils/setup.ts index f24a8e19de..cfc27eaee0 100644 --- a/blocksuite/integration-test/src/__tests__/utils/setup.ts +++ b/blocksuite/integration-test/src/__tests__/utils/setup.ts @@ -8,29 +8,36 @@ import { registerStoreSpecs, StoreExtensions, } from '@blocksuite/affine/extensions'; -import type { ExtensionType, Store, Transformer } from '@blocksuite/store'; - -import { effects } from '../../effects.js'; - -registerStoreSpecs(); -blocksEffects(); -effects(); - import type { DocMode } from '@blocksuite/affine/model'; import { AffineSchemas } from '@blocksuite/affine/schemas'; import { CommunityCanvasTextFonts, FontConfigExtension, } from '@blocksuite/affine/shared/services'; -import type { ViewportTurboRendererExtension } from '@blocksuite/affine-gfx-turbo-renderer'; +import { + type ViewportTurboRendererExtension, + ViewportTurboRendererIdentifier, +} from '@blocksuite/affine-gfx-turbo-renderer'; +import type { ExtensionType, Store, Transformer } from '@blocksuite/store'; import { Schema, Text } from '@blocksuite/store'; import { createAutoIncrementIdGenerator, TestWorkspace, } from '@blocksuite/store/test'; +import { effects } from '../../effects.js'; import { TestAffineEditorContainer } from '../../index.js'; +registerStoreSpecs(); +blocksEffects(); +effects(); + +export function getRenderer() { + return editor.std.get( + ViewportTurboRendererIdentifier + ) as ViewportTurboRendererExtension; +} + function createCollectionOptions() { const schema = new Schema(); const room = Math.random().toString(16).slice(2, 8); From 2cd0e7581018e78aba83dbed87c9574eaecaac09 Mon Sep 17 00:00:00 2001 From: Yifeng Wang Date: Mon, 14 Apr 2025 17:04:22 +0800 Subject: [PATCH 2/4] fix: flaky --- .../src/__tests__/edgeless/viewport-renderer.spec.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts b/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts index fd218e24cf..0ff58541b5 100644 --- a/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts +++ b/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts @@ -63,12 +63,8 @@ describe('viewport turbo renderer', () => { await wait(100); const renderer = getRenderer(); renderer.viewport.zooming$.next(false); - await wait(renderer.options.debounceTime + 100); - if (renderer.viewport.zoom <= renderer.options.zoomThreshold) { - expect(renderer.currentState).toBe('ready'); - } else { - expect(renderer.currentState).toBe('pending'); - } + await wait(renderer.options.debounceTime + 500); + expect(renderer.currentState).toBe('ready'); }); test('invalidation should reset state to pending', async () => { From e68947c792ae5fbd89e1efefe5f1060ccd6fe8c1 Mon Sep 17 00:00:00 2001 From: Yifeng Wang Date: Mon, 14 Apr 2025 11:01:42 +0800 Subject: [PATCH 3/4] fix(editor): integration vitest config warning --- blocksuite/integration-test/vitest.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/blocksuite/integration-test/vitest.config.ts b/blocksuite/integration-test/vitest.config.ts index 70cf1ce8c3..6676266232 100644 --- a/blocksuite/integration-test/vitest.config.ts +++ b/blocksuite/integration-test/vitest.config.ts @@ -18,10 +18,9 @@ export default defineConfig(_configEnv => browser: { enabled: true, headless: process.env.CI === 'true', - name: 'chromium', + instances: [{ browser: 'chromium' }], provider: 'playwright', isolate: false, - providerOptions: {}, viewport: { width: 1024, height: 768, From e9d04de3995d1795c12148be60d7abbab7f25f3c Mon Sep 17 00:00:00 2001 From: Yifeng Wang Date: Mon, 14 Apr 2025 11:07:48 +0800 Subject: [PATCH 4/4] test(editor): add tests for turbo renderer layout cache --- .../gfx/turbo-renderer/src/turbo-renderer.ts | 8 +-- ...enderer.spec.ts => turbo-renderer.spec.ts} | 53 ++++++++++++++----- 2 files changed, 43 insertions(+), 18 deletions(-) rename blocksuite/integration-test/src/__tests__/edgeless/{viewport-renderer.spec.ts => turbo-renderer.spec.ts} (56%) diff --git a/blocksuite/affine/gfx/turbo-renderer/src/turbo-renderer.ts b/blocksuite/affine/gfx/turbo-renderer/src/turbo-renderer.ts index b92c405d79..8465b4fae8 100644 --- a/blocksuite/affine/gfx/turbo-renderer/src/turbo-renderer.ts +++ b/blocksuite/affine/gfx/turbo-renderer/src/turbo-renderer.ts @@ -73,20 +73,16 @@ export const TurboRendererConfigFactory = export class ViewportTurboRendererExtension extends GfxExtension { static override key = 'viewportTurboRenderer'; - private readonly state$ = new BehaviorSubject('inactive'); + public readonly state$ = new BehaviorSubject('inactive'); public readonly canvas: HTMLCanvasElement = document.createElement('canvas'); + public layoutCacheData: ViewportLayoutTree | null = null; private readonly worker: Worker; private readonly disposables = new DisposableGroup(); - private layoutCacheData: ViewportLayoutTree | null = null; private layoutVersion = 0; private bitmap: ImageBitmap | null = null; private viewportElement: GfxViewportElement | null = null; private readonly refresh$ = new Subject(); - public get currentState(): RenderingState { - return this.state$.value; - } - constructor(gfx: GfxController) { super(gfx); diff --git a/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts b/blocksuite/integration-test/src/__tests__/edgeless/turbo-renderer.spec.ts similarity index 56% rename from blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts rename to blocksuite/integration-test/src/__tests__/edgeless/turbo-renderer.spec.ts index 0ff58541b5..3ac3b4f58e 100644 --- a/blocksuite/integration-test/src/__tests__/edgeless/viewport-renderer.spec.ts +++ b/blocksuite/integration-test/src/__tests__/edgeless/turbo-renderer.spec.ts @@ -2,6 +2,7 @@ * Please refer to integration-test/README.md for commands to run tests. */ import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph'; +import { noop } from '@blocksuite/affine/global/utils'; import { TurboRendererConfigFactory, ViewportTurboRendererExtension, @@ -45,36 +46,64 @@ describe('viewport turbo renderer', () => { test('initial state should be pending', async () => { const renderer = getRenderer(); - expect(renderer.currentState).toBe('pending'); + expect(renderer.state$.value).toBe('pending'); }); test('zooming should change state to zooming', async () => { const renderer = getRenderer(); renderer.viewport.zooming$.next(true); await wait(); - expect(renderer.currentState).toBe('zooming'); + expect(renderer.state$.value).toBe('zooming'); renderer.viewport.zooming$.next(false); await wait(); - expect(renderer.currentState).not.toBe('zooming'); + expect(renderer.state$.value).not.toBe('zooming'); }); - test('state should become ready after rendering', async () => { - addSampleNotes(doc, 1); - await wait(100); + test('state transitions between pending and ready', async () => { const renderer = getRenderer(); + + // Initial state should be pending after adding content + addSampleNotes(doc, 1); + await wait(100); // Short wait for initial processing + expect(renderer.state$.value).toBe('pending'); + + // Ensure zooming is off and wait for debounce + buffer renderer.viewport.zooming$.next(false); await wait(renderer.options.debounceTime + 500); - expect(renderer.currentState).toBe('ready'); + expect(renderer.state$.value).toBe('ready'); }); - test('invalidation should reset state to pending', async () => { + test('initial layout cache data should be null', () => { + const renderer = getRenderer(); + expect(renderer.layoutCacheData).toBeNull(); + }); + + test('invalidation should reset layout cache data to null', async () => { const renderer = getRenderer(); addSampleNotes(doc, 1); - expect(renderer.currentState).toBe('pending'); - await wait(renderer.options.debounceTime + 500); - expect(renderer.currentState).toBe('ready'); + await wait(100); + + // Access getter to populate cache + const _cache = renderer.layoutCache; + noop(_cache); + expect(renderer.layoutCacheData).not.toBeNull(); + + // Invalidate addSampleNotes(doc, 1); await wait(100); - expect(renderer.currentState).toBe('pending'); + + expect(renderer.layoutCacheData).toBeNull(); + }); + + test('accessing layoutCache getter should populate cache data', async () => { + const renderer = getRenderer(); + addSampleNotes(doc, 1); + await wait(); + expect(renderer.layoutCacheData).toBeNull(); // Check internal state before access + + const _cache = renderer.layoutCache; // Access getter to populate cache + noop(_cache); + expect(renderer.layoutCacheData).not.toBeNull(); // Check internal state after access + expect(renderer.layoutCache?.roots.length).toBeGreaterThan(0); // Check public getter result }); });