Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<img src="https://img.shields.io/badge/Storage-IndexedDB_v8-F59E0B" alt="IndexedDB v8">
<img src="https://img.shields.io/badge/PWA-v3.0-5BB974?logo=pwa" alt="PWA v3.0">
<img src="https://img.shields.io/badge/i18n-19_locales-2937_keys-0EA5E9" alt="i18n 19 locales — 2937 keys">
<img src="https://img.shields.io/badge/Tests-7356%2B_%2F_595_files-22C55E" alt="7356+ tests / 595 files">
<img src="https://img.shields.io/badge/Tests-7357%2B_%2F_595_files-22C55E" alt="7357+ tests / 595 files">
<img src="https://img.shields.io/codecov/c/github/qnbs/WorldScript-Studio?logo=codecov&label=Coverage" alt="Codecov Coverage">
<img src="https://img.shields.io/badge/License-MIT-22C55E" alt="License MIT">
<img src="https://img.shields.io/github/actions/workflow/status/qnbs/WorldScript-Studio/.github/workflows/ci.yml?branch=main&logo=github" alt="CI Status">
Expand Down Expand Up @@ -511,7 +511,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2937 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
| **Testing** | Vitest 4.x (7356+ tests / 595 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (7357+ tests / 595 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
| **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` |
Expand Down Expand Up @@ -549,7 +549,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (7356+ tests, 595 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7357+ tests, 595 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
Expand Down Expand Up @@ -711,7 +711,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
| `scorecard` | weekly + `main` push | OpenSSF Scorecard — SARIF uploaded to GitHub Code Scanning |

**Current test metrics (2026-08-30, source-synchronized; CI remains authoritative for pass/fail):**
- **7356+ unit tests** across **595 test files** — CI is authoritative for pass/fail
- **7357+ unit tests** across **595 test files** — CI is authoritative for pass/fail
- Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2937 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta)

Expand Down
5 changes: 3 additions & 2 deletions components/manuscript/ManuscriptEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const ManuscriptEditor: FC<{ isFocusMode: boolean }> = React.memo(({ isFo
// QNBS-v3: Defer highlight computation so keystroke → textarea updates stay synchronous even for long scenes.
const deferredContent = useDeferredValue(activeSection?.content ?? '');
const isHighlightPending = deferredContent !== (activeSection?.content ?? '');
// QNBS-v3: Pending mirror opacity preserves WCAG contrast while retaining the deferred-state cue.

// QNBS-v3 (#341): shared with components/ui/Textarea.tsx and ContextPanel.tsx — the raw enum
// value (e.g. 'custom') is not a valid font-family, and the highlight overlay must render the
Expand Down Expand Up @@ -398,7 +399,7 @@ export const ManuscriptEditor: FC<{ isFocusMode: boolean }> = React.memo(({ isFo
ref={highlightRef}
data-testid="manuscript-editor-mirror"
dir={dir}
className={`absolute inset-0 p-4 sm:p-6 md:p-12 pt-2 leading-relaxed pointer-events-none overflow-auto max-w-3xl mx-auto transition-all duration-500 ${isFocusMode ? 'max-w-4xl pt-12' : ''} ${isHighlightPending ? 'opacity-70' : 'opacity-100'}`}
className={`absolute inset-0 p-4 sm:p-6 md:p-12 pt-2 leading-relaxed pointer-events-none overflow-auto max-w-3xl mx-auto transition-all duration-500 ${isFocusMode ? 'max-w-4xl pt-12' : ''} ${isHighlightPending ? 'opacity-75' : 'opacity-100'}`}
style={editorStyles}
aria-hidden="true"
>
Expand Down Expand Up @@ -526,4 +527,4 @@ export const ManuscriptEditor: FC<{ isFocusMode: boolean }> = React.memo(({ isFo
</div>
);
});
ManuscriptEditor.displayName = 'ManuscriptEditor';
ManuscriptEditor.displayName = 'ManuscriptEditor';
38 changes: 37 additions & 1 deletion tests/unit/manuscript/ManuscriptEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';

// QNBS-v3: Retain the previous deferred value so pending-state coverage models React's stale render contract.
const reactDeferredMock = vi.hoisted(() => ({
forceStaleValue: false,
previousValue: undefined as unknown,
}));

vi.mock('react', async () => {
const actual = await vi.importActual<typeof import('react')>('react');
const useDeferredValue = <T,>(value: T): T => {
if (reactDeferredMock.forceStaleValue && reactDeferredMock.previousValue !== undefined) {
return reactDeferredMock.previousValue as T;
}
reactDeferredMock.previousValue = value;
return value;
};
return { ...actual, useDeferredValue };
});

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -180,6 +198,9 @@ describe('ManuscriptEditor', () => {
ltMock.available = false;
ltMock.matches = [];
ltMock.applySuggestion.mockReset();
// QNBS-v3: Reset deferred mock state so each test observes an isolated render timeline.
reactDeferredMock.forceStaleValue = false;
reactDeferredMock.previousValue = undefined;
});

it('shows empty state when no section is selected', () => {
Expand Down Expand Up @@ -321,5 +342,20 @@ describe('ManuscriptEditor', () => {
} as unknown as React.UIEvent<HTMLTextAreaElement>);
expect(mirror.scrollTop).toBe(360);
});

// QNBS-v3: A stale mirror must retain content and the WCAG-safe pending opacity during edits.
it('applies opacity-75 while retaining stale mirror content during deferred rendering', () => {
const { rerender } = render(<ManuscriptEditor isFocusMode={false} />);
mockActiveSection = {
id: 'sec-1',
title: 'Chapter One',
content: 'Updated content while the edit settles',
};
reactDeferredMock.forceStaleValue = true;
rerender(<ManuscriptEditor isFocusMode />);
const mirror = screen.getByTestId('manuscript-editor-mirror');
expect(mirror).toHaveClass('opacity-75');
expect(mirror).toHaveTextContent('Hello world teh quick brown fox');
});
});
});
});
Loading