Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
635f16b
refactor(manage): migrate rich text editor from Slate to Tiptap
rschlaefli Jul 7, 2026
e0734c9
feat(editor): add support for GFM tables and code syntax highlighting
rschlaefli Jul 7, 2026
0f99fe7
test(editor): add Playwright E2E spec for Tiptap rich features
rschlaefli Jul 7, 2026
7dc19c1
chore: merge remote-tracking branch 'origin/v3' into migrate-editor-t…
rschlaefli Jul 7, 2026
9830d1f
style: format get-shard-files script
rschlaefli Jul 7, 2026
c6bbe2c
fix(playwright): stabilize flaky answer options accordion and auto-sa…
rschlaefli Jul 7, 2026
a259f06
Merge remote-tracking branch 'origin/v3' into migrate-editor-to-tiptap
rschlaefli Jul 7, 2026
a2e9ba2
docs(wiki): update tech stack overview and playwright testing tips fo…
rschlaefli Jul 7, 2026
dd1cb04
docs(wiki): remove legacy host based stack and simplify agents.md
rschlaefli Jul 8, 2026
1b103dd
chore: merge v3 and address PR review feedback on Tiptap editor and s…
rschlaefli Jul 8, 2026
da9da9b
refactor: use standard data-cy attributes instead of data-testid
rschlaefli Jul 8, 2026
b40fc33
fix(a11y/styles/tests): address Droid review maintainability and acce…
rschlaefli Jul 8, 2026
a6ebc55
docs(project): add PR 5148 finalization plan
rschlaefli Jul 10, 2026
6c1b6eb
chore: merge v3 into Tiptap finalization
rschlaefli Jul 10, 2026
f336930
docs(project): record PR 5148 baseline
rschlaefli Jul 10, 2026
a417373
fix(editor): preserve form state during content sync
rschlaefli Jul 10, 2026
91d680e
test(playwright): stabilize Tiptap editor workflows
rschlaefli Jul 10, 2026
d4b2247
fix(editor): enforce Markdown-safe rich content
rschlaefli Jul 10, 2026
8975d20
fix(editor): synchronize focused external updates
rschlaefli Jul 10, 2026
f4a2736
fix(markdown): align shared preview styles
rschlaefli Jul 10, 2026
ebdb9e6
docs(project): record PR 5148 verification
rschlaefli Jul 10, 2026
fdd2c79
docs(project): finalize PR 5148 evidence
rschlaefli Jul 10, 2026
f2a6c79
test(playwright): focus Tiptap table cell before typing
rschlaefli Jul 10, 2026
9a4c805
test(playwright): align submission switch expectation
rschlaefli Jul 10, 2026
59453d5
refactor(editor): simplify toolbar interactions
rschlaefli Jul 10, 2026
c2849f5
fix(editor): normalize legacy empty content
rschlaefli Jul 11, 2026
9e04380
fix(editor): refresh localized placeholder
rschlaefli Jul 11, 2026
837b057
refactor(editor): simplify final migration diff
rschlaefli Jul 11, 2026
9b18a04
fix(editor): react to Tiptap selection state
rschlaefli Jul 12, 2026
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
6 changes: 5 additions & 1 deletion .agents/skills/klicker-playwright-e2e/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ Action menus:

Editors and selects:

- Use `fillEditorField` and `verifyEditorField` for Slate/rich-text fields.
- Use `.ProseMirror` (with `.first()`/`.last()`) to target the Tiptap rich-text editor DOM.
- For Slate/legacy rich-text fields, `fillEditorField` and `verifyEditorField` helpers remain.
- Use direct `toHaveValue` for ordinary inputs; do not wrap simple fields just for symmetry.
- Use `switchElementType` and `setElementStatus` for element modal dropdowns when applicable.
- Prefer `selectOption` from repo helpers for design-system selects.
Expand Down Expand Up @@ -129,6 +130,9 @@ Cleanup dialogs:
- **react-select**: target the inner `<input>` via `#container-id input` for `.fill()`/`.press()`/visibility assertions — Cypress `.type()` works on the wrapper, Playwright does not. (`playwright/tests/K-elements-selection.spec.ts`)
- **localforage parity**: Playwright creates a fresh context per test (Cypress keeps IndexedDB across `it` blocks). Serial workflows depending on previous PWA answers must snapshot/restore localforage — and direct QR links may need restoration on the `https://pwa.klicker.com` origin, not `127.0.0.1`. (`playwright/util/workflow.ts`)
- **PIN-cookie bridges**: clear test-side PIN cookie bridges whenever the Cypress source clears cookies, or later direct-link checks bypass the expected PIN form via a stale `live-quiz-pin-*` cookie. (`playwright/tests/O-live-quiz.spec.ts`)
- **Tiptap Editor Layout Shifts & Accordion Clicks**: The Tiptap editor mounts asynchronously and shifts the surrounding layout (toolbar rendering, element height). When clicking accordion triggers (such as `open-answer-collection-options`) shortly after mounting or element modal opening, always wait for the `.ProseMirror` element to be visible first and implement a retry loop to click the accordion.
- **Tiptap Auto-Save / False Dirty States**: Empty questions or initial content sync can trigger false-positive form dirty-states (and "Data Recovery" prompts) due to trivial formatting differences (carriage returns, trailing newlines). Use `normalizeMarkdown` (in `ContentInput.tsx`) to trim and sanitize before comparing the external string against `editor.getMarkdown()`.
- **Tiptap Code Block Language Selection**: To assert correct syntax highlighting (Highlight.js in the editor, Prism.js in the preview `/questions/[id]`), do not use toolbar button terminal clicks (which omit the language tag). Instead, type the markdown input rule (e.g. ` ```js ` followed by a space/Enter) directly into the editor.

## CI Notes

Expand Down
60 changes: 31 additions & 29 deletions .github/scripts/get-shard-files.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,70 @@
const fs = require('fs');
const path = require('path');
const fs = require('fs')
const path = require('path')

const shardIndex = parseInt(process.argv[2], 10);
const shardIndex = parseInt(process.argv[2], 10)
if (isNaN(shardIndex) || shardIndex < 1 || shardIndex > 5) {
console.error('Invalid shard index. Must be between 1 and 5.');
process.exit(1);
console.error('Invalid shard index. Must be between 1 and 5.')
process.exit(1)
}

const testsDir = path.join(__dirname, '../../playwright/tests');
const allFiles = fs.readdirSync(testsDir).filter(file => file.endsWith('.spec.ts'));
const testsDir = path.join(__dirname, '../../playwright/tests')
const allFiles = fs
.readdirSync(testsDir)
.filter((file) => file.endsWith('.spec.ts'))

// Load timings from playwright/timings.json
const timingsPath = path.join(__dirname, '../../playwright/timings.json');
let timings = { durations: [] };
const timingsPath = path.join(__dirname, '../../playwright/timings.json')
let timings = { durations: [] }
if (fs.existsSync(timingsPath)) {
try {
timings = JSON.parse(fs.readFileSync(timingsPath, 'utf8'));
timings = JSON.parse(fs.readFileSync(timingsPath, 'utf8'))
} catch (err) {
console.error('Error parsing timings.json:', err);
console.error('Error parsing timings.json:', err)
}
}

// Map of spec file to duration
const durationMap = new Map();
const durationMap = new Map()
for (const entry of timings.durations) {
// Normalize spec path to just the filename
const baseName = path.basename(entry.spec);
durationMap.set(baseName, entry.duration);
const baseName = path.basename(entry.spec)
durationMap.set(baseName, entry.duration)
}

// Map each file to its estimated/historical duration
const filesWithDuration = allFiles.map(file => {
const filesWithDuration = allFiles.map((file) => {
return {
file,
duration: durationMap.has(file) ? durationMap.get(file) : 30 // default to 30s for new tests
};
});
duration: durationMap.has(file) ? durationMap.get(file) : 30, // default to 30s for new tests
}
})

// Sort files by duration descending for the greedy bin-packing algorithm
filesWithDuration.sort((a, b) => b.duration - a.duration);
filesWithDuration.sort((a, b) => b.duration - a.duration)

// Initialize 5 shards
const numShards = 5;
const numShards = 5
const shards = Array.from({ length: numShards }, () => ({
files: [],
totalDuration: 0
}));
totalDuration: 0,
}))

// Distribute files using greedy bin-packing
for (const item of filesWithDuration) {
// Find the shard with the smallest total duration
let minShard = shards[0];
let minShard = shards[0]
for (let i = 1; i < numShards; i++) {
if (shards[i].totalDuration < minShard.totalDuration) {
minShard = shards[i];
minShard = shards[i]
}
}
minShard.files.push(item.file);
minShard.totalDuration += item.duration;
minShard.files.push(item.file)
minShard.totalDuration += item.duration
}

// Select the files for the requested shard
const targetFiles = shards[shardIndex - 1].files;
const targetFiles = shards[shardIndex - 1].files

// Map files to their relative path from the playwright package directory
const relativePaths = targetFiles.map(file => `tests/${file}`);
console.log(relativePaths.join(' '));
const relativePaths = targetFiles.map((file) => `tests/${file}`)
console.log(relativePaths.join(' '))
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ cypress/ # E2E tests
| Layer | Technology |
| ---------------------- | ----------------------------------------------------- |
| Frontend framework | Next.js 15, React, TypeScript |
| Rich text editor | Tiptap (v3) (Markdown-native) |
| Styling | TailwindCSS, @uzh-bf/design-system |
| GraphQL server | GraphQL Yoga + Pothos schema builder |
| GraphQL client | Apollo Client |
Expand Down
13 changes: 9 additions & 4 deletions apps/frontend-manage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
"@klicker-uzh/types": "workspace:*",
"@socialgouv/matomo-next": "1.9.1",
"@tanstack/react-table": "8.20.5",
"@tiptap/core": "3.27.1",
"@tiptap/extension-code-block-lowlight": "3.27.1",
"@tiptap/extension-image": "3.27.1",
"@tiptap/extension-placeholder": "3.27.1",
"@tiptap/extension-table": "3.27.1",
"@tiptap/markdown": "3.27.1",
"@tiptap/react": "3.27.1",
"@tiptap/starter-kit": "3.27.1",
"@uidotdev/usehooks": "2.4.1",
"@uzh-bf/design-system": "4.1.6",
"dayjs": "1.11.20",
Expand All @@ -40,6 +48,7 @@
"js-search": "2.0.1",
"localforage": "1.10.0",
"lodash": "4.17.21",
"lowlight": "3.3.0",
"nanoid": "5.0.8",
"next": "15.5.18",
"next-intl": "4.3.4",
Expand All @@ -54,12 +63,8 @@
"react-select": "5.10.2",
"react-tagcloud": "2.3.3",
"recharts": "3.0.2",
"remark-slate": "1.8.6",
"remeda": "2.15.0",
"sharp": "0.33.5",
"slate": "0.110.2",
"slate-history": "0.109.0",
"slate-react": "0.110.2",
"tailwind-merge": "3.3.1",
"yup": "1.6.1"
},
Expand Down
Loading
Loading