From 39724c2df7afdcb03e7ed42374be72cfc001d651 Mon Sep 17 00:00:00 2001 From: Jason Kummerl Date: Fri, 20 Feb 2026 16:38:21 -0500 Subject: [PATCH 1/4] fix(test): use \w+ instead of .+ in e2e capture pattern The greedy .+ pattern captured the entire text2 string, so there were no insert segments to assert. Constraining to \w+ captures only "Jason", leaving "coding" vs "hacking" as a real deviation. Co-Authored-By: Claude Opus 4.6 --- tests/expected-patterns.test.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/expected-patterns.test.ts b/tests/expected-patterns.test.ts index cc25f6f..83629eb 100644 --- a/tests/expected-patterns.test.ts +++ b/tests/expected-patterns.test.ts @@ -17,15 +17,10 @@ test.describe('Expected Patterns', () => { }) test('non-captured deviations still show as insert/remove', async ({ page }) => { - // Use a simple template where we can have both expected captures and deviations - await page.getByTestId('text1').fill('Hello (?.+) end') - await page.getByTestId('text2').fill('Goodbye World end') - // "World" is captured as expected, but "Hello" vs "Goodbye" is a real deviation - // Actually regex won't match because "Hello" != "Goodbye" literal... - // Use: template matches but has surrounding diffs from diff algorithm - await page.getByTestId('text1').fill('(?.+) likes coding') + // Use \w+ instead of .+ so the capture is constrained to "Jason" only, + // leaving "coding" vs "hacking" as a real deviation for insert/remove + await page.getByTestId('text1').fill('(?\\w+) likes coding') await page.getByTestId('text2').fill('Jason likes hacking') - // "Jason" is captured, "coding" vs "hacking" is a real deviation await expect( page.getByTestId('diff-result').locator('.diff-expected').first() ).toBeVisible() From bf85395797d07770357c3d305b3e48f8a567c892 Mon Sep 17 00:00:00 2001 From: Jason Kummerl Date: Fri, 20 Feb 2026 16:38:29 -0500 Subject: [PATCH 2/4] chore(trunk): ignore eslint on .svelte.ts files svelte-eslint-parser does not implement scopeManager.addGlobals() required by ESLint 10. Only affects .svelte.ts runes modules; regular .ts and .svelte files lint fine. Co-Authored-By: Claude Opus 4.6 --- .trunk/trunk.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 8de022b..0717a7e 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -24,6 +24,9 @@ lint: - linters: [markdownlint] paths: - .changeset/** + - linters: [eslint] + paths: + - '**/*.svelte.ts' definitions: - name: prettier files: From d58dc3d06b6605330a09e0ca9926646a2e21d46d Mon Sep 17 00:00:00 2001 From: Jason Kummerl Date: Fri, 20 Feb 2026 17:49:11 -0500 Subject: [PATCH 3/4] ci: add run-tests workflow with unit and e2e jobs Runs on pull requests to main when src/tests change. Unit tests run first, then sharded Playwright e2e tests across 2 shards. Uses pnpm, caches build/vitest/playwright artifacts. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/run-tests.yml | 147 ++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 .github/workflows/run-tests.yml diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..1ac029b --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,147 @@ +name: Run tests + +permissions: + contents: read + +on: + workflow_call: + pull_request: + branches: + - main + - '!dependabot/**' + paths: + - src/** + - tests/** + - '!docs/**' + - package.json + - package-lock.json + - svelte.config.* + - vite.config.* + - playwright.config.* + - tsconfig*.json + - .github/workflows/run-tests.yml + +concurrency: + group: run-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit-tests: + runs-on: blacksmith-2vcpu-ubuntu-2404 # trunk-ignore(actionlint/runner-label) + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 # zizmor: ignore[unpinned-uses] + with: + persist-credentials: false + fetch-depth: 1 + + - uses: pnpm/action-setup@v4 # zizmor: ignore[unpinned-uses] + with: + version: 10 + + - name: Use Node.js - 24 + uses: actions/setup-node@v6 # zizmor: ignore[unpinned-uses] + with: + node-version: 24 + + - name: Cache build artifacts + uses: actions/cache@v5 # zizmor: ignore[unpinned-uses] + with: + path: | + .svelte-kit + dist + key: ${{ runner.os }}-build-${{ hashFiles('src/**/*', 'pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Cache vitest artifacts + uses: actions/cache@v5 # zizmor: ignore[unpinned-uses] + with: + path: | + node_modules/.vitest + test-results + key: ${{ runner.os }}-vitest-${{ hashFiles('pnpm-lock.yaml', 'src/**/*.{test,spec}.{js,ts}', 'tests/**/*.{test,spec}.{js,ts}', 'vitest.config.ts') }} + restore-keys: | + ${{ runner.os }}-vitest- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run unit tests + run: | + pnpm build + pnpm test + + - name: Upload unit test results + if: always() + uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses] + with: + name: unit-test-results + path: | + coverage/** + test-results/** + if-no-files-found: ignore + retention-days: 7 + + e2e-tests: + needs: unit-tests + runs-on: blacksmith-2vcpu-ubuntu-2404 # trunk-ignore(actionlint/runner-label) + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + include: + - shard: 1/2 + shard_name: 1-2 + - shard: 2/2 + shard_name: 2-2 + steps: + - name: Checkout + uses: actions/checkout@v6 # zizmor: ignore[unpinned-uses] + with: + persist-credentials: false + fetch-depth: 1 + + - uses: pnpm/action-setup@v4 # zizmor: ignore[unpinned-uses] + with: + version: 10 + + - name: Use Node.js - 24 + uses: actions/setup-node@v6 # zizmor: ignore[unpinned-uses] + with: + node-version: 24 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Cache Playwright browsers + uses: actions/cache@v5 # zizmor: ignore[unpinned-uses] + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps + + - name: Sanity check - list discovered tests + run: pnpm exec playwright test --list | head -n 50 | cat + + - name: Run e2e tests (sharded) + env: + CI: true + run: pnpm exec playwright test --config=playwright.config.ts --shard=${{ matrix.shard }} --reporter=line + + - name: Upload Playwright artifacts + if: always() + uses: actions/upload-artifact@v4 # zizmor: ignore[unpinned-uses] + with: + name: playwright-artifacts-${{ matrix.shard_name }} + path: | + playwright-report/** + test-results/** + blob-report/** + if-no-files-found: ignore + retention-days: 7 From cd3157ad8cd2a802e69e089800f34f1782d1a649 Mon Sep 17 00:00:00 2001 From: Jason Kummerl Date: Fri, 20 Feb 2026 18:18:57 -0500 Subject: [PATCH 4/4] fix(test): use getByTestId for reliable fills in webkit Replace page.fill('[data-testid=...]') with page.getByTestId().fill() to fix webkit race condition where CSS selector fills didn't update Svelte reactive state before assertions fired. Co-Authored-By: Claude Opus 4.6 --- tests/default.test.ts | 8 ++++---- tests/snippets.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/default.test.ts b/tests/default.test.ts index 94fd200..661c0ad 100644 --- a/tests/default.test.ts +++ b/tests/default.test.ts @@ -6,16 +6,16 @@ test.describe('SvelteDiffMatchPatch', () => { }) test('renders a visible diff', async ({ page }) => { - await page.fill('[data-testid="text1"]', 'hello world') - await page.fill('[data-testid="text2"]', 'hello brave world') + await page.getByTestId('text1').fill('hello world') + await page.getByTestId('text2').fill('hello brave world') await expect(page.getByTestId('diff-result')).toContainText('brave') await expect(page.getByTestId('diff-result')).toContainText('hello') await expect(page.getByTestId('diff-result')).toContainText('world') }) test('applies custom rendererClasses', async ({ page }) => { - await page.fill('[data-testid="text1"]', 'foo shoo') - await page.fill('[data-testid="text2"]', 'bar shoo') + await page.getByTestId('text1').fill('foo shoo') + await page.getByTestId('text2').fill('bar shoo') await expect(page.getByTestId('diff-result').locator('.diff-remove')).toBeVisible() await expect(page.getByTestId('diff-result').locator('.diff-insert')).toBeVisible() await expect(page.getByTestId('diff-result').locator('.diff-equal')).toBeVisible() diff --git a/tests/snippets.test.ts b/tests/snippets.test.ts index e515eee..8c1f320 100644 --- a/tests/snippets.test.ts +++ b/tests/snippets.test.ts @@ -16,8 +16,8 @@ test.describe('SvelteDiffMatchPatch', () => { }) test('applies custom rendererClasses', async ({ page }) => { - await page.fill('[data-testid="text1"]', 'foo shoo') - await page.fill('[data-testid="text2"]', 'bar shoo') + await page.getByTestId('text1').fill('foo shoo') + await page.getByTestId('text2').fill('bar shoo') await expect(page.getByTestId('diff-result').locator('.diff-remove')).toBeVisible() await expect(page.getByTestId('diff-result').locator('.diff-insert')).toBeVisible() await expect(page.getByTestId('diff-result').locator('.diff-equal')).toBeVisible()