@peaske7/readit 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +118 -76
- package/.claude/commands/review.md +1 -1
- package/.claude/roadmap.md +32 -9
- package/.claude/user-stories.md +100 -15
- package/AGENTS.md +30 -26
- package/Makefile +32 -0
- package/README.md +90 -2
- package/biome.json +18 -8
- package/bun.lock +426 -568
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +56 -1
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
- package/e2e/comments.spec.ts +14 -58
- package/e2e/document-load.spec.ts +1 -23
- package/e2e/export.spec.ts +4 -4
- package/e2e/perf/add-comment.spec.ts +9 -11
- package/e2e/perf/fixtures/generate.ts +1 -5
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/utils/metrics.ts +73 -9
- package/e2e/persistence-file.spec.ts +41 -26
- package/e2e/utils/selection.ts +17 -73
- package/go/cmd/readit/main.go +416 -0
- package/go/go.mod +20 -0
- package/go/go.sum +41 -0
- package/go/internal/server/anchor.go +302 -0
- package/go/internal/server/anchor_test.go +111 -0
- package/go/internal/server/comments.go +390 -0
- package/go/internal/server/documents.go +113 -0
- package/go/internal/server/embed.go +17 -0
- package/go/internal/server/headings.go +33 -0
- package/go/internal/server/headings_test.go +75 -0
- package/go/internal/server/htmltext.go +123 -0
- package/go/internal/server/markdown.go +157 -0
- package/go/internal/server/markdown_bench_test.go +42 -0
- package/go/internal/server/markdown_test.go +79 -0
- package/go/internal/server/server.go +453 -0
- package/go/internal/server/server_bench_test.go +122 -0
- package/go/internal/server/settings.go +110 -0
- package/go/internal/server/sse.go +140 -0
- package/go/internal/server/storage.go +275 -0
- package/go/internal/server/storage_test.go +118 -0
- package/go/internal/server/template.go +66 -0
- package/go/internal/server/types.go +101 -0
- package/go/internal/server/watcher.go +74 -0
- package/index.html +4 -14
- package/nvim-readit/lua/readit/health.lua +64 -0
- package/nvim-readit/lua/readit/init.lua +463 -0
- package/nvim-readit/plugin/readit.lua +19 -0
- package/package.json +20 -28
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +881 -0
- package/src/cli.ts +183 -21
- package/src/components/ActionsMenu.svelte +95 -0
- package/src/components/CommentBadge.svelte +67 -0
- package/src/components/CommentErrorBanner.svelte +33 -0
- package/src/components/CommentInput.svelte +75 -0
- package/src/components/CommentListItem.svelte +95 -0
- package/src/components/CommentManager.svelte +129 -0
- package/src/components/CommentNav.svelte +109 -0
- package/src/components/DocumentViewer.svelte +218 -0
- package/src/components/FloatingComment.svelte +107 -0
- package/src/components/Header.svelte +76 -0
- package/src/components/InlineEditor.svelte +72 -0
- package/src/components/MarginNote.svelte +167 -0
- package/src/components/MarginNotesContainer.svelte +33 -0
- package/src/components/RawModal.svelte +126 -0
- package/src/components/ReanchorConfirm.svelte +30 -0
- package/src/components/SettingsModal.svelte +220 -0
- package/src/components/ShortcutCapture.svelte +82 -0
- package/src/components/ShortcutList.svelte +145 -0
- package/src/components/TabBar.svelte +52 -0
- package/src/components/TableOfContents.svelte +125 -0
- package/src/components/ui/ActionLink.svelte +40 -0
- package/src/components/ui/{Button.tsx → Button.svelte} +19 -20
- package/src/components/ui/Dialog.svelte +97 -0
- package/src/components/ui/DropdownMenu.svelte +85 -0
- package/src/components/ui/DropdownMenuItem.svelte +38 -0
- package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
- package/src/components/ui/{Text.tsx → Text.svelte} +18 -23
- package/src/env.d.ts +6 -0
- package/src/index.css +36 -166
- package/src/lib/__fixtures__/bench-data.ts +0 -13
- package/src/lib/anchor.bench.ts +1 -12
- package/src/lib/anchor.test.ts +0 -8
- package/src/lib/anchor.ts +0 -4
- package/src/lib/comment-storage.bench.ts +49 -0
- package/src/lib/comment-storage.test.ts +41 -33
- package/src/lib/comment-storage.ts +21 -18
- package/src/lib/export.bench.ts +21 -0
- package/src/lib/export.ts +0 -1
- package/src/{hooks/useHeadings.test.ts → lib/headings.test.ts} +10 -24
- package/src/{hooks/useHeadings.ts → lib/headings.ts} +11 -13
- package/src/lib/highlight/core.test.ts +0 -5
- package/src/lib/highlight/dom.ts +52 -216
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +112 -132
- package/src/lib/highlight/resolver.ts +5 -79
- package/src/lib/highlight/types.ts +0 -5
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +26 -0
- package/src/lib/i18n/ja.ts +26 -0
- package/src/lib/i18n/types.ts +25 -0
- package/src/lib/margin-layout.bench.ts +61 -0
- package/src/lib/margin-layout.ts +0 -7
- package/src/lib/markdown-renderer.test.ts +154 -0
- package/src/lib/markdown-renderer.ts +177 -0
- package/src/lib/mermaid-config.ts +38 -0
- package/src/lib/mermaid-renderer.ts +162 -0
- package/src/lib/mermaid-worker.ts +60 -0
- package/src/lib/positions.ts +31 -24
- package/src/lib/shortcut-registry.ts +244 -0
- package/src/lib/utils.ts +0 -29
- package/src/main.ts +16 -0
- package/src/schema.ts +16 -5
- package/src/server.ts +355 -91
- package/src/stores/app.svelte.ts +231 -0
- package/src/stores/locale.svelte.ts +46 -0
- package/src/stores/settings.svelte.ts +90 -0
- package/src/stores/shortcuts.svelte.ts +104 -0
- package/src/stores/ui.svelte.ts +12 -0
- package/src/template.ts +104 -0
- package/src/test-setup.ts +47 -0
- package/svelte.config.js +5 -0
- package/tsconfig.json +2 -2
- package/vite.config.ts +23 -3
- package/vscode-readit/.mcp.json +7 -0
- package/vscode-readit/.vscodeignore +7 -0
- package/vscode-readit/bun.lock +78 -0
- package/vscode-readit/icon.svg +10 -0
- package/vscode-readit/package.json +110 -0
- package/vscode-readit/src/extension.ts +117 -0
- package/vscode-readit/src/server-manager.ts +272 -0
- package/vscode-readit/src/webview-provider.ts +204 -0
- package/vscode-readit/tsconfig.json +20 -0
- package/e2e/fixtures/sample.html +0 -13
- package/src/App.tsx +0 -368
- package/src/components/ActionsMenu.tsx +0 -91
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -230
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -136
- package/src/components/Header.tsx +0 -54
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -185
- package/src/components/MarginNotes.tsx +0 -23
- package/src/components/RawModal.tsx +0 -144
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -232
- package/src/components/TabBar.tsx +0 -60
- package/src/components/TableOfContents.tsx +0 -108
- package/src/components/comments/CommentBadge.tsx +0 -49
- package/src/components/comments/CommentInput.tsx +0 -86
- package/src/components/comments/CommentListItem.tsx +0 -90
- package/src/components/comments/CommentManager.tsx +0 -129
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionLink.tsx +0 -28
- package/src/components/ui/Dialog.tsx +0 -116
- package/src/components/ui/DropdownMenu.tsx +0 -158
- package/src/contexts/CommentContext.tsx +0 -198
- package/src/contexts/LocaleContext.tsx +0 -76
- package/src/contexts/PositionsContext.tsx +0 -16
- package/src/contexts/SettingsContext.tsx +0 -133
- package/src/hooks/useClickOutside.ts +0 -31
- package/src/hooks/useCommentNavigation.ts +0 -107
- package/src/hooks/useComments.ts +0 -311
- package/src/hooks/useDocument.ts +0 -157
- package/src/hooks/useScrollSpy.ts +0 -77
- package/src/hooks/useTextSelection.ts +0 -86
- package/src/lib/highlight/worker.ts +0 -45
- package/src/main.tsx +0 -13
- package/src/store.ts +0 -222
|
@@ -6,6 +6,7 @@ export interface LoadMetrics {
|
|
|
6
6
|
fcp: number | null;
|
|
7
7
|
domContentLoaded: number;
|
|
8
8
|
allHighlightsPainted: number;
|
|
9
|
+
pageReady: number | null;
|
|
9
10
|
highlightCount: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -45,11 +46,10 @@ export async function waitForHighlightCount(
|
|
|
45
46
|
let lastChangeTime = performance.now();
|
|
46
47
|
|
|
47
48
|
const check = () => {
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
[
|
|
51
|
-
|
|
52
|
-
const count = uniqueIds.size;
|
|
49
|
+
// Use the CSS Custom Highlight API observability hook
|
|
50
|
+
const highlights = (window as unknown as Record<string, unknown>)
|
|
51
|
+
.__readitHighlights as { commentIds: string[] } | undefined;
|
|
52
|
+
const count = highlights?.commentIds?.length ?? 0;
|
|
53
53
|
|
|
54
54
|
// Exact target reached
|
|
55
55
|
if (count >= expected) {
|
|
@@ -88,6 +88,48 @@ export async function waitForHighlightCount(
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// ─── Wait for positions ─────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Waits until Positions.cache() has run AFTER highlights were painted.
|
|
95
|
+
* The `afterTimestamp` ensures we don't capture the initial empty cache()
|
|
96
|
+
* that runs before highlights exist.
|
|
97
|
+
*/
|
|
98
|
+
async function waitForPositionsReady(
|
|
99
|
+
page: Page,
|
|
100
|
+
afterTimestamp: number,
|
|
101
|
+
timeoutMs = 60_000,
|
|
102
|
+
): Promise<number> {
|
|
103
|
+
return page.evaluate(
|
|
104
|
+
({ after, timeout }) => {
|
|
105
|
+
return new Promise<number>((resolve, reject) => {
|
|
106
|
+
const deadline = performance.now() + timeout;
|
|
107
|
+
|
|
108
|
+
const check = () => {
|
|
109
|
+
const ts = (window as unknown as Record<string, unknown>)
|
|
110
|
+
.__readitPositionsReady as number | undefined;
|
|
111
|
+
|
|
112
|
+
// Only resolve if positions were computed AFTER highlights painted
|
|
113
|
+
if (ts !== undefined && ts > after) {
|
|
114
|
+
resolve(ts);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (performance.now() > deadline) {
|
|
119
|
+
reject(new Error("Timed out waiting for positions to be ready"));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
requestAnimationFrame(check);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
requestAnimationFrame(check);
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
{ after: afterTimestamp, timeout: timeoutMs },
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
91
133
|
// ─── Collect load metrics ────────────────────────────────────────────
|
|
92
134
|
|
|
93
135
|
/**
|
|
@@ -105,16 +147,36 @@ export async function collectLoadMetrics(
|
|
|
105
147
|
expectedComments,
|
|
106
148
|
);
|
|
107
149
|
|
|
150
|
+
// Wait for positions to be computed AFTER highlights
|
|
151
|
+
// (captures the forced reflow cost from getBoundingClientRect)
|
|
152
|
+
let pageReadyTimestamp: number | null;
|
|
153
|
+
try {
|
|
154
|
+
pageReadyTimestamp = await waitForPositionsReady(
|
|
155
|
+
page,
|
|
156
|
+
highlightTimestamp,
|
|
157
|
+
10_000,
|
|
158
|
+
);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (
|
|
161
|
+
!(error instanceof Error) ||
|
|
162
|
+
!error.message.includes("Timed out waiting for positions")
|
|
163
|
+
) {
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
// Positions may not fire in all configurations
|
|
167
|
+
pageReadyTimestamp = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
108
170
|
const navMetrics = await page.evaluate(() => {
|
|
109
171
|
const nav = performance.getEntriesByType(
|
|
110
172
|
"navigation",
|
|
111
173
|
)[0] as PerformanceNavigationTiming;
|
|
112
174
|
const paintEntries = performance.getEntriesByType("paint");
|
|
113
175
|
const fcp = paintEntries.find((e) => e.name === "first-contentful-paint");
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
[
|
|
117
|
-
|
|
176
|
+
// Use CSS Custom Highlight API observability hook
|
|
177
|
+
const highlights = (window as unknown as Record<string, unknown>)
|
|
178
|
+
.__readitHighlights as { commentIds: string[] } | undefined;
|
|
179
|
+
const actualCount = highlights?.commentIds?.length ?? 0;
|
|
118
180
|
|
|
119
181
|
return {
|
|
120
182
|
fcp: fcp ? fcp.startTime : null,
|
|
@@ -127,6 +189,7 @@ export async function collectLoadMetrics(
|
|
|
127
189
|
fcp: navMetrics.fcp,
|
|
128
190
|
domContentLoaded: navMetrics.domContentLoaded,
|
|
129
191
|
allHighlightsPainted: highlightTimestamp,
|
|
192
|
+
pageReady: pageReadyTimestamp,
|
|
130
193
|
highlightCount: navMetrics.actualCount,
|
|
131
194
|
};
|
|
132
195
|
}
|
|
@@ -240,6 +303,7 @@ export function reportLoadMetrics(
|
|
|
240
303
|
` FCP: ${metrics.fcp !== null ? `${Math.round(metrics.fcp)}ms` : "N/A"}`,
|
|
241
304
|
` DOM Content Loaded: ${Math.round(metrics.domContentLoaded)}ms`,
|
|
242
305
|
` All highlights painted: ${Math.round(metrics.allHighlightsPainted)}ms`,
|
|
306
|
+
` Page ready (+ layout): ${metrics.pageReady !== null ? `${Math.round(metrics.pageReady)}ms` : "N/A (positions timed out)"}`,
|
|
243
307
|
` Highlights found: ${metrics.highlightCount}`,
|
|
244
308
|
];
|
|
245
309
|
console.log(lines.join("\n"));
|
|
@@ -57,7 +57,7 @@ test.describe("File-Based Comment Persistence", () => {
|
|
|
57
57
|
await page.goto(url);
|
|
58
58
|
|
|
59
59
|
// Wait for document to load
|
|
60
|
-
const article = page.locator("article");
|
|
60
|
+
const article = page.locator("article#document-content");
|
|
61
61
|
await expect(article).toBeVisible();
|
|
62
62
|
|
|
63
63
|
// Add a comment
|
|
@@ -96,7 +96,7 @@ test.describe("File-Based Comment Persistence", () => {
|
|
|
96
96
|
try {
|
|
97
97
|
await page.goto(url);
|
|
98
98
|
|
|
99
|
-
const article = page.locator("article");
|
|
99
|
+
const article = page.locator("article#document-content");
|
|
100
100
|
await expect(article).toBeVisible();
|
|
101
101
|
|
|
102
102
|
// Add a comment
|
|
@@ -119,8 +119,14 @@ test.describe("File-Based Comment Persistence", () => {
|
|
|
119
119
|
await expect(page.locator("body")).toContainText(commentText);
|
|
120
120
|
|
|
121
121
|
// Verify highlight still exists
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
await page.waitForFunction(
|
|
123
|
+
() => {
|
|
124
|
+
const h = (window as unknown as Record<string, unknown>)
|
|
125
|
+
.__readitHighlights as { commentIds: string[] } | undefined;
|
|
126
|
+
return h && h.commentIds.length > 0;
|
|
127
|
+
},
|
|
128
|
+
{ timeout: 10_000 },
|
|
129
|
+
);
|
|
124
130
|
} finally {
|
|
125
131
|
await cleanup();
|
|
126
132
|
}
|
|
@@ -140,7 +146,7 @@ test.describe("File-Based Comment Persistence", () => {
|
|
|
140
146
|
try {
|
|
141
147
|
await page.goto(url1);
|
|
142
148
|
|
|
143
|
-
const article = page.locator("article");
|
|
149
|
+
const article = page.locator("article#document-content");
|
|
144
150
|
await expect(article).toBeVisible();
|
|
145
151
|
|
|
146
152
|
await selectTextInArticle(page, "testing text selection");
|
|
@@ -164,15 +170,21 @@ test.describe("File-Based Comment Persistence", () => {
|
|
|
164
170
|
try {
|
|
165
171
|
await page.goto(url2);
|
|
166
172
|
|
|
167
|
-
const article = page.locator("article");
|
|
173
|
+
const article = page.locator("article#document-content");
|
|
168
174
|
await expect(article).toBeVisible();
|
|
169
175
|
|
|
170
176
|
// Comment should still exist from previous session
|
|
171
177
|
await expect(page.locator("body")).toContainText(commentText);
|
|
172
178
|
|
|
173
179
|
// Highlight should still exist
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
await page.waitForFunction(
|
|
181
|
+
() => {
|
|
182
|
+
const h = (window as unknown as Record<string, unknown>)
|
|
183
|
+
.__readitHighlights as { commentIds: string[] } | undefined;
|
|
184
|
+
return h && h.commentIds.length > 0;
|
|
185
|
+
},
|
|
186
|
+
{ timeout: 10_000 },
|
|
187
|
+
);
|
|
176
188
|
} finally {
|
|
177
189
|
await cleanup2();
|
|
178
190
|
}
|
|
@@ -214,15 +226,19 @@ Pre-existing comment to be cleared.
|
|
|
214
226
|
try {
|
|
215
227
|
await page.goto(url);
|
|
216
228
|
|
|
217
|
-
const article = page.locator("article");
|
|
229
|
+
const article = page.locator("article#document-content");
|
|
218
230
|
await expect(article).toBeVisible();
|
|
219
231
|
|
|
220
232
|
// Wait for clean operation to complete
|
|
221
233
|
await page.waitForTimeout(500);
|
|
222
234
|
|
|
223
|
-
// Verify no
|
|
224
|
-
const
|
|
225
|
-
|
|
235
|
+
// Verify no highlights via observability hook
|
|
236
|
+
const highlightCount = await page.evaluate(() => {
|
|
237
|
+
const h = (window as unknown as Record<string, unknown>)
|
|
238
|
+
.__readitHighlights as { commentIds: string[] } | undefined;
|
|
239
|
+
return h?.commentIds?.length ?? 0;
|
|
240
|
+
});
|
|
241
|
+
expect(highlightCount).toBe(0);
|
|
226
242
|
} finally {
|
|
227
243
|
await cleanup();
|
|
228
244
|
}
|
|
@@ -237,7 +253,7 @@ Pre-existing comment to be cleared.
|
|
|
237
253
|
try {
|
|
238
254
|
await page.goto(url);
|
|
239
255
|
|
|
240
|
-
const article = page.locator("article");
|
|
256
|
+
const article = page.locator("article#document-content");
|
|
241
257
|
await expect(article).toBeVisible();
|
|
242
258
|
|
|
243
259
|
// Add initial comment
|
|
@@ -256,26 +272,25 @@ Pre-existing comment to be cleared.
|
|
|
256
272
|
let fileContent = readFileSync(commentPath, "utf-8");
|
|
257
273
|
expect(fileContent).toContain(initialComment);
|
|
258
274
|
|
|
259
|
-
// Find the margin note
|
|
260
|
-
const marginNote = page
|
|
261
|
-
.locator(".group")
|
|
262
|
-
.filter({ hasText: textToSelect })
|
|
263
|
-
.first();
|
|
264
|
-
await marginNote.hover();
|
|
275
|
+
// Find the margin note by data-comment-id (first one)
|
|
276
|
+
const marginNote = page.locator("[data-comment-id]").first();
|
|
265
277
|
|
|
266
|
-
//
|
|
267
|
-
const editButton = marginNote.
|
|
268
|
-
await editButton.click();
|
|
278
|
+
// Force-click edit button (may be hidden behind opacity-0 group-hover)
|
|
279
|
+
const editButton = marginNote.getByText("Edit").first();
|
|
280
|
+
await editButton.click({ force: true });
|
|
269
281
|
|
|
270
|
-
// Wait for edit mode
|
|
271
|
-
const textarea =
|
|
282
|
+
// Wait for edit mode — find textarea globally since hasText filter breaks in edit mode
|
|
283
|
+
const textarea = page.locator("[data-comment-id] textarea");
|
|
272
284
|
await expect(textarea).toBeVisible();
|
|
273
285
|
|
|
274
286
|
// Clear and type new text
|
|
275
287
|
await textarea.fill("Updated comment text");
|
|
276
288
|
|
|
277
289
|
// Save edit
|
|
278
|
-
const saveButton =
|
|
290
|
+
const saveButton = page
|
|
291
|
+
.locator("[data-comment-id]")
|
|
292
|
+
.getByText("Save")
|
|
293
|
+
.first();
|
|
279
294
|
await saveButton.click();
|
|
280
295
|
|
|
281
296
|
// Wait for file to be updated
|
|
@@ -298,7 +313,7 @@ Pre-existing comment to be cleared.
|
|
|
298
313
|
try {
|
|
299
314
|
await page.goto(url);
|
|
300
315
|
|
|
301
|
-
const article = page.locator("article");
|
|
316
|
+
const article = page.locator("article#document-content");
|
|
302
317
|
await expect(article).toBeVisible();
|
|
303
318
|
|
|
304
319
|
// Add a comment
|
package/e2e/utils/selection.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wait for the Svelte app to be fully mounted and interactive.
|
|
5
|
+
* The app sets data-readit-ready="true" on <html> after onMount.
|
|
6
|
+
*/
|
|
7
|
+
export async function waitForAppReady(page: Page): Promise<void> {
|
|
8
|
+
await page.waitForFunction(
|
|
9
|
+
() => document.documentElement.dataset.readitReady === "true",
|
|
10
|
+
{ timeout: 10_000 },
|
|
11
|
+
);
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
/**
|
|
4
15
|
* Select text within an article element (for markdown documents)
|
|
@@ -8,9 +19,12 @@ export async function selectTextInArticle(
|
|
|
8
19
|
page: Page,
|
|
9
20
|
textToSelect: string,
|
|
10
21
|
): Promise<void> {
|
|
22
|
+
// Ensure the Svelte app is fully mounted before dispatching events
|
|
23
|
+
await waitForAppReady(page);
|
|
24
|
+
|
|
11
25
|
// Find text and calculate offsets, then dispatch custom event
|
|
12
26
|
await page.evaluate((text) => {
|
|
13
|
-
const article = document.querySelector("article");
|
|
27
|
+
const article = document.querySelector("article#document-content");
|
|
14
28
|
if (!article) throw new Error("Article element not found");
|
|
15
29
|
|
|
16
30
|
const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
|
|
@@ -39,78 +53,10 @@ export async function selectTextInArticle(
|
|
|
39
53
|
throw new Error(`Text "${text}" not found in article`);
|
|
40
54
|
}, textToSelect);
|
|
41
55
|
|
|
42
|
-
// Wait for
|
|
56
|
+
// Wait for Svelte to process the selection
|
|
43
57
|
await page.waitForTimeout(100);
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
/**
|
|
47
|
-
* Select text within an iframe (for HTML documents)
|
|
48
|
-
* Uses custom event to trigger selection handler (same as markdown)
|
|
49
|
-
*/
|
|
50
|
-
export async function selectTextInIframe(
|
|
51
|
-
page: Page,
|
|
52
|
-
_iframe: FrameLocator,
|
|
53
|
-
textToSelect: string,
|
|
54
|
-
): Promise<void> {
|
|
55
|
-
// Get the actual frame object for evaluation
|
|
56
|
-
const frame = page.frame({ url: /^about:srcdoc/ }) || page.frames()[1];
|
|
57
|
-
if (!frame) throw new Error("Could not find iframe frame");
|
|
58
|
-
|
|
59
|
-
// Calculate offsets in iframe, then dispatch custom event to parent
|
|
60
|
-
const offsets = await frame.evaluate((text) => {
|
|
61
|
-
const body = document.body;
|
|
62
|
-
const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
|
63
|
-
let currentOffset = 0;
|
|
64
|
-
|
|
65
|
-
while (walker.nextNode()) {
|
|
66
|
-
const textNode = walker.currentNode as Text;
|
|
67
|
-
const content = textNode.textContent || "";
|
|
68
|
-
const index = content.indexOf(text);
|
|
69
|
-
|
|
70
|
-
if (index !== -1) {
|
|
71
|
-
const startOffset = currentOffset + index;
|
|
72
|
-
const endOffset = startOffset + text.length;
|
|
73
|
-
return { text, startOffset, endOffset };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
currentOffset += content.length;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
throw new Error(`Text "${text}" not found in iframe`);
|
|
80
|
-
}, textToSelect);
|
|
81
|
-
|
|
82
|
-
// Dispatch custom event to parent window (IframeContainer listens for this)
|
|
83
|
-
await page.evaluate((detail) => {
|
|
84
|
-
const event = new CustomEvent("test:select-text", { detail });
|
|
85
|
-
window.dispatchEvent(event);
|
|
86
|
-
}, offsets);
|
|
87
|
-
|
|
88
|
-
// Wait for React to process state update
|
|
89
|
-
await page.waitForTimeout(200);
|
|
90
|
-
|
|
91
|
-
// Manually send applyHighlights to iframe (workaround for isReadyRef timing)
|
|
92
|
-
// This ensures the iframe gets the highlight even if React's useEffect hasn't fired
|
|
93
|
-
await page.evaluate(
|
|
94
|
-
(selection) => {
|
|
95
|
-
const iframe = document.querySelector("iframe");
|
|
96
|
-
if (iframe?.contentWindow) {
|
|
97
|
-
iframe.contentWindow.postMessage(
|
|
98
|
-
{
|
|
99
|
-
type: "applyHighlights",
|
|
100
|
-
comments: [],
|
|
101
|
-
pendingSelection: selection,
|
|
102
|
-
},
|
|
103
|
-
"*",
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
{ startOffset: offsets.startOffset, endOffset: offsets.endOffset },
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
// Wait for iframe to apply highlights
|
|
111
|
-
await page.waitForTimeout(200);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
60
|
/**
|
|
115
61
|
* Add a comment to the current selection
|
|
116
62
|
* Assumes CommentInputArea is visible
|
|
@@ -119,8 +65,6 @@ export async function addComment(
|
|
|
119
65
|
page: Page,
|
|
120
66
|
commentText: string,
|
|
121
67
|
): Promise<void> {
|
|
122
|
-
// Wait for the comment input textarea to appear
|
|
123
|
-
// Give more time for iframe postMessage round-trip
|
|
124
68
|
const textarea = page.locator('textarea[placeholder="Add your comment..."]');
|
|
125
69
|
await textarea.waitFor({ state: "visible", timeout: 10000 });
|
|
126
70
|
|