@typeroll/mcp-server 0.45.77 → 0.45.79
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/dist/bundled-content.js +22 -4
- package/dist/tools/deploy.js +2 -1
- package/dist/tools/media.js +1 -1
- package/dist/tools/migration.js +4 -2
- package/dist/tools/pages.js +1 -1
- package/dist/tools/preview.js +1 -1
- package/dist/tools/working-copy.js +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace.js +3 -4
- package/package.json +1 -1
- package/skills/tr-header-footer.md +95 -41
- package/skills/tr-migrate-wp.md +22 -6
- package/skills/tr-new-site.md +5 -7
- package/workspace-templates/qa/checklist.md +35 -0
- package/workspace-templates/qa/typeroll-platform-feedback.md +11 -0
- package/workspace-templates/scripts/README.md +85 -0
- package/workspace-templates/scripts/typeroll/browser.cjs +50 -0
- package/workspace-templates/scripts/typeroll/call.py +12 -0
- package/workspace-templates/scripts/typeroll/capture_text.cjs +17 -0
- package/workspace-templates/scripts/typeroll/card-stacking-check.cjs +27 -0
- package/workspace-templates/scripts/typeroll/check_assets.py +72 -0
- package/workspace-templates/scripts/typeroll/check_unsaved.py +13 -0
- package/workspace-templates/scripts/typeroll/check_url_parity.py +74 -0
- package/workspace-templates/scripts/typeroll/compare_text.py +59 -0
- package/workspace-templates/scripts/typeroll/contact_sheet.py +20 -0
- package/workspace-templates/scripts/typeroll/layout_trace.cjs +33 -0
- package/workspace-templates/scripts/typeroll/shoot_hosted.cjs +26 -0
- package/workspace-templates/scripts/typeroll/shootframe.cjs +16 -0
- package/workspace-templates/scripts/typeroll/sweep_blocks.example.py +23 -0
- package/workspace-templates/scripts/typeroll/trlib.py +212 -0
- package/workspace-templates/workflows/design.md +30 -0
- package/workspace-templates/workflows/migration.md +49 -0
package/dist/tools/deploy.js
CHANGED
|
@@ -15,6 +15,7 @@ export const deployTools = [
|
|
|
15
15
|
description: "Enqueue a deploy (static-page build → hosting) of the currently active version. Returns a job_id immediately; poll get_deploy_status until the status leaves queued/running. NOT for previewing iterative design/content edits — that's what get_preview_link is for (it renders live from the DB with no build). Reserve trigger_deploy for publishing, a stakeholder link to the compiled site, or pre-merge validation. Deploys build SAVED content only — unsaved drafts (working copies) are excluded, so commit_working_copy (or save:true on your writes) before deploying, or your changes won't ship. Customer publishing freezes a consistent saved source, then resumes source/Git/build-dispatch checkpoints without recapturing later edits. GitHub or Cloudflare performs rendering and media work. With dry_run:true, customer publishing validates frozen source without a Git push or external build; it does not prove an Astro build succeeds. Other self-hosted adapters may build locally without upload. Check the reported phase.",
|
|
16
16
|
inputSchema: {
|
|
17
17
|
environment: z.enum(['production', 'staging']).optional(),
|
|
18
|
+
allow_unsaved: z.boolean().optional().describe('Explicitly publish saved content while leaving the reported unsaved Pages/partials untouched. Only after owner confirmation.'),
|
|
18
19
|
dry_run: z.boolean().optional().describe('Validate without publishing. Customer publishing checks frozen source only; other adapters may build locally without upload.'),
|
|
19
20
|
version: versionParam,
|
|
20
21
|
},
|
|
@@ -40,7 +41,7 @@ export const deployTools = [
|
|
|
40
41
|
},
|
|
41
42
|
{
|
|
42
43
|
name: 'get_deploy_status',
|
|
43
|
-
description: "Status of a single deploy job. Returns { job, queued_for_seconds, queue_timeout_seconds }. job.status is one of queued | running | succeeded | failed. On a failed build, job.error names the failed step and the cause the step printed, job.failure.code is the reported code (e.g. media_build_process_exit_1) and job.failure.diagnostic holds { cause, lines } (the last 40 lines that step printed, credentials removed). On the Cloudflare build engine job.failure.provider_log also holds { step, build_id, summary, lines } from the provider's build log (last 60 lines). Read them before retrying; retrying does not fix a cause that is in the content or the build engine. Polling cadence: ~5s for queued/running. job.render_report, when present, reports actual HTML mode (full/partial), rendered, reused, total and removed route counts plus a fallback reason. From Core 0.2.17, custom blocks and consumed image metadata are tracked per route; older caches require one full build. It does not measure media or provider startup savings. job.coordinator, when present, reports the last completed CMS phase and duration_ms for that attempt; external build waiting is excluded. Keep observing the same job across checkpoints; do not trigger duplicate deploys. A job stuck in 'queued' for longer than queue_timeout_seconds (default 300s) auto-flips to failed with phase='queue_timeout' — so a single poll past that timestamp returns the terminal state and you can stop polling.",
|
|
44
|
+
description: "Status of a single deploy job. Optional top-level build_progress reports the exact task status, attempt, step and completed/total files; not an overall percentage or ETA. Returns { job, queued_for_seconds, queue_timeout_seconds }. job.status is one of queued | running | succeeded | failed. On a failed build, job.error names the failed step and the cause the step printed, job.failure.code is the reported code (e.g. media_build_process_exit_1) and job.failure.diagnostic holds { cause, lines } (the last 40 lines that step printed, credentials removed). On the Cloudflare build engine job.failure.provider_log also holds { step, build_id, summary, lines } from the provider's build log (last 60 lines). Read them before retrying; retrying does not fix a cause that is in the content or the build engine. Polling cadence: ~5s for queued/running. job.render_report, when present, reports actual HTML mode (full/partial), rendered, reused, total and removed route counts plus a fallback reason. From Core 0.2.17, custom blocks and consumed image metadata are tracked per route; older caches require one full build. It does not measure media or provider startup savings. job.coordinator, when present, reports the last completed CMS phase and duration_ms for that attempt; external build waiting is excluded. Keep observing the same job across checkpoints; do not trigger duplicate deploys. A job stuck in 'queued' for longer than queue_timeout_seconds (default 300s) auto-flips to failed with phase='queue_timeout' — so a single poll past that timestamp returns the terminal state and you can stop polling.",
|
|
44
45
|
inputSchema: { job_id: z.string(), include_provider: z.boolean().optional().describe('Also read the exact Git commit and Cloudflare build stages for a customer Git publication. Does not return build secrets.') },
|
|
45
46
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
46
47
|
const res = await client.get(siteId, `deploys/${encodeURIComponent(args.job_id)}`);
|
package/dist/tools/media.js
CHANGED
|
@@ -206,7 +206,7 @@ export const mediaTools = [
|
|
|
206
206
|
},
|
|
207
207
|
{
|
|
208
208
|
name: 'finalize_media',
|
|
209
|
-
description: 'Post-upload finalize for a single image: (1) applies immutable Cache-Control (public, max-age=31536000) to the R2 original via in-place CopyObject so the CDN caches it for a year, and (2) generates AVIF/WebP responsive variants if not already present. Call this immediately after a successful upload (the presigned PUT does NOT set cache headers — without finalize, Cloudflare defaults to ~4h and every visit re-fetches the original). Idempotent: safe to call twice. Skips variant work when variants already exist on the doc. INTEGRITY: the result
|
|
209
|
+
description: 'Post-upload finalize for a single image: (1) applies immutable Cache-Control (public, max-age=31536000) to the R2 original via in-place CopyObject so the CDN caches it for a year, and (2) generates AVIF/WebP responsive variants if not already present. Call this immediately after a successful upload (the presigned PUT does NOT set cache headers — without finalize, Cloudflare defaults to ~4h and every visit re-fetches the original). Idempotent: safe to call twice. Skips variant work when variants already exist on the doc. INTEGRITY: the response is { ok, result: { sha256, size_bytes, ... } }; read result.sha256 and result.size_bytes for the stored original — compare against your source file to catch transit corruption before referencing the asset. Better: pass expected_sha256 (hash the file yourself first, e.g. `shasum -a 256 file`) and the server rejects a mismatch with a 422 so corrupted bytes never get blessed with immutable cache headers. Truncated SVGs are rejected outright.',
|
|
210
210
|
inputSchema: {
|
|
211
211
|
media_id: z.string(),
|
|
212
212
|
expected_sha256: z.string().regex(/^[0-9a-fA-F]{64}$/).optional()
|
package/dist/tools/migration.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
3
|
-
const STATUS = z.enum(['migrated', 'redirected', 'excluded', 'unhandled']);
|
|
3
|
+
const STATUS = z.enum(['migrated', 'redirected', 'excluded', 'unhandled', 'matched_draft']);
|
|
4
4
|
const PLAIN_TEXT_REPAIR_FIELD = z.enum(['title', 'seo_title', 'seo_description', 'excerpt']);
|
|
5
5
|
export const migrationTools = [
|
|
6
6
|
{
|
|
@@ -8,6 +8,8 @@ export const migrationTools = [
|
|
|
8
8
|
description: 'Import an explicit sitemap URL into the migration URL inventory. Sitemap indexes are followed recursively; URLs outside source_origin are rejected and reported.',
|
|
9
9
|
inputSchema: {
|
|
10
10
|
url: z.string().url().describe('Absolute URL of a sitemap or sitemap index.'),
|
|
11
|
+
include_wordpress: z.boolean().optional().describe('Also enumerate public WordPress REST pages/posts/types. No helper installation required; incomplete discovery is reported.'),
|
|
12
|
+
crawl_pages: z.number().int().min(0).max(50).optional().describe('With include_wordpress, crawl up to this many source pages for same-origin legacy links. Default 0; bounded discovery is not an authoritative old-slug export.'),
|
|
11
13
|
source_origin: z.string().url().optional().describe('Expected legacy-site origin. Defaults to the sitemap origin.'),
|
|
12
14
|
},
|
|
13
15
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
@@ -69,7 +71,7 @@ export const migrationTools = [
|
|
|
69
71
|
},
|
|
70
72
|
{
|
|
71
73
|
name: 'list_migration_urls',
|
|
72
|
-
description: "The legacy site's URL inventory with LIVE coverage status. Every entry is classified on read against the site's current pages + redirects: `migrated` (a Page answers at that path), `redirected` (a redirect rule covers it), `excluded` (signed off as an intentional 404), `unhandled` (nothing covers it — the work list). Returns a summary over the whole inventory plus a page of entries, sorted worst-first then by GSC clicks. Use `status: \"unhandled\"` to get exactly what's left to do before cutover. Coverage is computed, never stored, so it's current the moment you create a redirect.",
|
|
74
|
+
description: "The legacy site's URL inventory with LIVE coverage status. Every entry is classified on read against the site's current pages + redirects: `migrated` (a Page answers at that path), `redirected` (a redirect rule covers it), `excluded` (signed off as an intentional 404), `matched_draft` (a saved but unpublished Page owns the path; not launch-ready), `unhandled` (nothing covers it — the work list). Returns a summary over the whole inventory plus a page of entries, sorted worst-first then by GSC clicks. Use `status: \"unhandled\"` to get exactly what's left to do before cutover. Coverage is computed, never stored, so it's current the moment you create a redirect.",
|
|
73
75
|
inputSchema: {
|
|
74
76
|
status: STATUS.optional().describe('Only return entries with this coverage status.'),
|
|
75
77
|
limit: z.number().int().positive().max(1000).optional().describe('Default 200.'),
|
package/dist/tools/pages.js
CHANGED
|
@@ -223,7 +223,7 @@ export const pageTools = [
|
|
|
223
223
|
},
|
|
224
224
|
{
|
|
225
225
|
name: 'batch_update_pages',
|
|
226
|
-
description: 'Apply per-page patches in one call (up to 200 entries). Each entry is { page_id, patch, save?, answer_sources? }; failures are reported per-row, the rest still apply. BUFFER MODEL: content patches land in each page\'s unsaved draft; per-entry save:true (or the top-level save flag) commits — typical for a user-approved sweep. content_mode is rejected per-row; switch each page with set_page_mode.',
|
|
226
|
+
description: 'Apply per-page patches in one call (up to 200 entries). Each entry is { page_id, patch, save?, answer_sources? }; failures are reported per-row, the rest still apply. BUFFER MODEL: content patches land in each page\'s unsaved draft; per-entry save:true (or the top-level save flag) commits — typical for a user-approved sweep. Returns results with saved (whether this patch reached saved storage), staged (draft field names), and applied_immediately (status/schedule field names). A status-only patch is saved immediately and does not commit earlier drafts. content_mode is rejected per-row; switch each page with set_page_mode.',
|
|
227
227
|
inputSchema: {
|
|
228
228
|
updates: z
|
|
229
229
|
.array(z.object({
|
package/dist/tools/preview.js
CHANGED
|
@@ -17,7 +17,7 @@ export const previewTools = [
|
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
name: 'get_preview_link',
|
|
20
|
-
description: 'Mint a signed URL the user (or your own browser tool) can open to SEE the rendered preview. Renders LIVE from the database with NO build — this is the PREFERRED way to preview design/content changes as you iterate; reach for this, not trigger_deploy, when previewing. BUFFER MODEL: your edits are unsaved drafts, so for the iteration loop mint the link with include_working_copy:true (drafts visible); a plain link shows SAVED content only — right for stakeholders reviewing what will deploy. Mint ONCE and REUSE that single URL across edits — internal links keep the token so it navigates the whole branch, and it stays valid until the TTL lapses (24h default and max). Target a page (page_id), or a raw slug; omit all for the home page. Forms on the page can be clicked through end to end — validation, steps, success message or the named redirect target — without creating submissions or running actions ("Preview – nothing is sent"). (For a permanent public link, complete Publishing setup and wait for a verified deployment on the organization or site domain.)',
|
|
20
|
+
description: 'Returns url (normal wrapper) and frame_url (signed sandboxed frame for rendering-only QA; use the wrapper for Extension host interactions). Mint a signed URL the user (or your own browser tool) can open to SEE the rendered preview. Renders LIVE from the database with NO build — this is the PREFERRED way to preview design/content changes as you iterate; reach for this, not trigger_deploy, when previewing. BUFFER MODEL: your edits are unsaved drafts, so for the iteration loop mint the link with include_working_copy:true (drafts visible); a plain link shows SAVED content only — right for stakeholders reviewing what will deploy. Mint ONCE and REUSE that single URL across edits — internal links keep the token so it navigates the whole branch, and it stays valid until the TTL lapses (24h default and max). Target a page (page_id), or a raw slug; omit all for the home page. Forms on the page can be clicked through end to end — validation, steps, success message or the named redirect target — without creating submissions or running actions ("Preview – nothing is sent"). (For a permanent public link, complete Publishing setup and wait for a verified deployment on the organization or site domain.)',
|
|
21
21
|
inputSchema: {
|
|
22
22
|
page_id: z.string().optional(),
|
|
23
23
|
slug: z.string().optional(),
|
|
@@ -29,7 +29,7 @@ function wcPath(t) {
|
|
|
29
29
|
export const workingCopyTools = [
|
|
30
30
|
{
|
|
31
31
|
name: 'read_working_copy',
|
|
32
|
-
description: 'Read a doc\'s unsaved draft (working copy) as a raw field diff — { working_copy: null } means everything is saved. read_page/get_page_blocks already return the draft VIEW; use this when you need to know exactly WHICH fields are unsaved, e.g. before discarding (the draft may hold the user\'s in-progress editor work, not just yours).',
|
|
32
|
+
description: 'Templates save immediately and do not use this tool. Use target: {kind: page|partial, id: ...}, not page_id. Read a doc\'s unsaved draft (working copy) as a raw field diff — { working_copy: null } means everything is saved. read_page/get_page_blocks already return the draft VIEW; use this when you need to know exactly WHICH fields are unsaved, e.g. before discarding (the draft may hold the user\'s in-progress editor work, not just yours).',
|
|
33
33
|
inputSchema: {
|
|
34
34
|
target: wcTargetSchema,
|
|
35
35
|
version: versionParam,
|
package/dist/version.js
CHANGED
package/dist/workspace.js
CHANGED
|
@@ -18,7 +18,7 @@ export const workspaceSchema = z.object({
|
|
|
18
18
|
export const defaultWorkspace = { schema_version: 1, portal: 'https://app.typeroll.com', organization_id: null, site_id: null, version: 'main', tool_mode: 'compact' };
|
|
19
19
|
const lockSchema = z.object({ schema_version: z.literal(1), generator_version: z.string(), files: z.record(z.string(), z.string().regex(/^[a-f0-9]{64}$/)) }).strict();
|
|
20
20
|
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
21
|
-
export const ignores = ['.typeroll/', '.env', '.env.*', '!.env.example', '.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json', 'qa/reports/', 'sources/private/'];
|
|
21
|
+
export const ignores = ['.typeroll/', '.env', '.env.*', '!.env.example', '.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json', 'qa/reports/', 'sources/private/', '**/__pycache__/', 'scripts/typeroll/node_modules/'];
|
|
22
22
|
export const agentInstructions = `# Working on this website
|
|
23
23
|
|
|
24
24
|
Read typeroll.json, brief/site.md, brief/rules.md and decisions.md first. Read
|
|
@@ -79,7 +79,8 @@ or launch acceptance. Compare text and design/images separately: preserved dimen
|
|
|
79
79
|
renewed dimensions to their approved brief. New design never permits rewriting preserved text. Start with workflows/migration.md for the kickoff
|
|
80
80
|
prompt. The agent rebuilds imported HTML using ordinary block tools; the owner
|
|
81
81
|
does not need to manually convert each Page. No automatic converter is available.
|
|
82
|
-
Discover tools and recipes on demand.
|
|
82
|
+
Discover tools and recipes on demand. Read scripts/README.md for version-bound
|
|
83
|
+
pre-deploy and hosted QA tools. Follow workflows/design.md for layout defaults.
|
|
83
84
|
|
|
84
85
|
## Project rules and search strategy
|
|
85
86
|
|
|
@@ -115,8 +116,6 @@ export const workspaceTemplates = {
|
|
|
115
116
|
'workflows/README.md': "# Project routines\n\nRead only the relevant routine and discover current tool schemas through MCP.\nProject files record intent; the CMS owns current content and configuration.\n\n| Task | Project context | On-demand recipe |\n| --- | --- | --- |\n| Guided start | workflows/interview.md, brief/interview.md | Ask one question at a time; resume confirmed answers |\n| New website | workflows/new-site.md, brief/site.md, brief/brand.md, brief/content-model.md | tr-new-site, tr-starter-design |\n| Migration | workflows/migration.md, brief/migration.md | tr-migration-evidence, then tr-migrate-wp or the matching source recipe |\n| Everyday editing | brief/rules.md and the relevant Page | tr-edit-site |\n| SEO | brief/seo.md and current site settings | tr-seo |\n| New reusable block | brief/brand.md and recorded editing needs | tr-custom-blocks |\n| Verification and publication | qa/checklist.md, decisions.md | Current preview/deploy tools and publishing readiness |\n\nAdd project-specific routines here with inputs, steps, acceptance evidence and approval boundaries. Do not copy the complete Typeroll manual or private app guides. Discover enabled app guidance with read_app_documentation. Load a recipe when needed so instructions stay current and context stays small.\n",
|
|
116
117
|
'decisions.md': '# Decisions\n\nRecord date, decision, reason, approved scope and alternatives when useful.\n',
|
|
117
118
|
'sources/README.md': '# Sources\n\nStore research references and copy proposals. Record source URLs and dates.\nPrivate extracts belong in ignored private/ or an approved external store.\nDo not treat local drafts as current CMS content or commit media libraries.\n',
|
|
118
|
-
'scripts/README.md': '# Project automation\n\nKeep repeatable imports and QA scripts here. Use the authenticated REST API\nfor scripted work; keep credentials outside files and inject them at runtime.\nMCP remains the interactive interface. Typeroll currently has no general site CRUD CLI.\n',
|
|
119
|
-
'qa/checklist.md': '# Verification\n\n- Confirm Site, Version and deployed/source identity.\n- Read back saved content and inspect desktop/mobile previews.\n- Exercise menus, keyboard focus, links and enabled app states.\n- Verify routes, metadata, media and publishing readiness before launch.\n- Check that structured data (FAQ, recipe, event) matches what each page shows.\n- Record tested pages, viewports, results and unresolved issues.\n- Store screenshots and private runtime reports in ignored reports/.\n- Publishing validation does not prove visual or editorial completeness.\n',
|
|
120
119
|
};
|
|
121
120
|
export async function readWorkspace(dir) {
|
|
122
121
|
try {
|
package/package.json
CHANGED
|
@@ -5,11 +5,10 @@ description: Build accessible native header and footer partials with the site lo
|
|
|
5
5
|
|
|
6
6
|
# Native header and footer compositions
|
|
7
7
|
|
|
8
|
-
Use block-mode partials for new work.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
tenant HTML, CSS, or JavaScript.
|
|
8
|
+
Use block-mode partials for new work. Prefer `get_composition_starter`
|
|
9
|
+
for the current native header tree: `template/site_logo`, `core/navigation_menu`
|
|
10
|
+
and `core/navigation_links`. Use the menu's dialog/drawer rather than an inline
|
|
11
|
+
mobile expansion that moves the logo. Existing `core/navigation` remains supported.
|
|
13
12
|
|
|
14
13
|
## Preconditions
|
|
15
14
|
|
|
@@ -31,51 +30,106 @@ read it back. Localize both accessible labels and every link label.
|
|
|
31
30
|
[
|
|
32
31
|
{
|
|
33
32
|
"type": "core/section",
|
|
34
|
-
"data": {
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
33
|
+
"data": {
|
|
34
|
+
"width": "wide",
|
|
35
|
+
"padding_y": "compact"
|
|
36
|
+
},
|
|
37
|
+
"children": [
|
|
38
|
+
{
|
|
39
|
+
"type": "core/container",
|
|
40
|
+
"data": {
|
|
41
|
+
"direction": "row",
|
|
42
|
+
"wrap": "nowrap",
|
|
43
|
+
"align_main": "space-between",
|
|
44
|
+
"align_cross": "center",
|
|
45
|
+
"width": "full"
|
|
46
|
+
},
|
|
47
|
+
"children": [
|
|
48
|
+
{
|
|
49
|
+
"type": "template/site_logo",
|
|
50
|
+
"data": {
|
|
51
|
+
"height": "md",
|
|
52
|
+
"link_to_home": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "core/navigation_menu",
|
|
57
|
+
"data": {
|
|
58
|
+
"aria_label": "Main navigation",
|
|
59
|
+
"menu_label": "Open menu",
|
|
60
|
+
"close_label": "Close menu",
|
|
61
|
+
"collapse_below": "1024"
|
|
62
|
+
},
|
|
63
|
+
"slots": [
|
|
64
|
+
[
|
|
65
|
+
{
|
|
66
|
+
"type": "core/navigation_links",
|
|
67
|
+
"data": {
|
|
68
|
+
"direction": "row",
|
|
69
|
+
"links": [
|
|
70
|
+
{
|
|
71
|
+
"label": "Home",
|
|
72
|
+
"href": "/"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"label": "About",
|
|
76
|
+
"href": "/about/"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"label": "Contact",
|
|
80
|
+
"href": "/contact/"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
{
|
|
88
|
+
"type": "core/navigation_links",
|
|
89
|
+
"data": {
|
|
90
|
+
"direction": "column",
|
|
91
|
+
"links": [
|
|
92
|
+
{
|
|
93
|
+
"label": "Home",
|
|
94
|
+
"href": "/"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"label": "About",
|
|
98
|
+
"href": "/about/"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"label": "Contact",
|
|
102
|
+
"href": "/contact/"
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
57
108
|
]
|
|
58
109
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
62
113
|
}
|
|
63
114
|
]
|
|
64
115
|
```
|
|
65
116
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
visible and
|
|
117
|
+
The first slot is desktop; the second is the optional mobile composition.
|
|
118
|
+
The default is drawer below 1024 px and inline from 1024 px. For responsive
|
|
119
|
+
link fields use matching `responsive_breakpoints` (tablet 768, laptop 1024,
|
|
120
|
+
desktop 1280, wide 1536); do not independently hide all links at the menu boundary.
|
|
121
|
+
Verify 1023/1024 px, Tab/Shift-Tab, Enter/Space, Escape returning focus,
|
|
122
|
+
visible focus, long labels, no-JavaScript links and no horizontal overflow.
|
|
72
123
|
|
|
73
124
|
## Footer tree and consent
|
|
74
125
|
|
|
75
|
-
Use
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
Use an accessible footer navigation landmark and `core/navigation_links`, not
|
|
127
|
+
a second header drawer. If the footer has running about text, give it a separate,
|
|
128
|
+
wider column beside a nested link grid. An equal four-column grid is rarely a
|
|
129
|
+
good fit for one dense paragraph and three short link lists. Start with
|
|
130
|
+
`core/columns` ratio `1-2`, put flow containers inside the slots, and tune the
|
|
131
|
+
gap and readable text width to the actual content. Stack on mobile. Inspect at
|
|
132
|
+
390, 1024, 1280 and 1536 px; do not copy one site's pixel values blindly.
|
|
79
133
|
|
|
80
134
|
Cookie consent is a site feature, not a footer widget. Configure it separately:
|
|
81
135
|
|
package/skills/tr-migrate-wp.md
CHANGED
|
@@ -148,12 +148,28 @@ place. Do not preserve them as classed raw HTML in text blocks.
|
|
|
148
148
|
|
|
149
149
|
For each source URL:
|
|
150
150
|
|
|
151
|
-
a.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
a. Choose access based on missing evidence, not a mandatory plugin install:
|
|
152
|
+
|
|
153
|
+
| Source requirement | Access path |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| Public Pages, posts, media and rendered builder content exposed in REST | Public paginated `/wp-json/wp/v2/pages`, `/posts`, `/media` and rendered HTML can suffice |
|
|
156
|
+
| SEO exposed in `yoast_head_json`, visible menus/backgrounds | Public REST + rendered HTML/CSS; verify actual coverage |
|
|
157
|
+
| Private/draft content, REST-hidden types, ACF/custom fields, exact menus | Authenticated helper/export when available and authorized |
|
|
158
|
+
| REST blocked by security policy | Owner-approved helper/export; do not bypass access controls |
|
|
159
|
+
| Authoritative old slugs or plugin redirects | Owner export/helper **if supported**; a public crawl only approximates them |
|
|
160
|
+
|
|
161
|
+
Do not ask the owner to install the helper when public sources cover the scope.
|
|
162
|
+
Inventory both Pages and posts (including sitemap-excluded public records),
|
|
163
|
+
crawl internal links and inspect redirects, menus and CSS background images.
|
|
164
|
+
Do not claim the helper exports a field without reading its current contract.
|
|
165
|
+
The helper is distributed through Typeroll's documented package; do not claim
|
|
166
|
+
WordPress.org availability. Listing it there is separate publication work.
|
|
167
|
+
|
|
168
|
+
b. Clean the HTML. Parse HTML and discard comments before extracting text or
|
|
169
|
+
constructing blocks (comments may contain old phone-plugin metadata). Do not
|
|
170
|
+
use regex tag removal as a text parser. Resolve relative `../` links against
|
|
171
|
+
the source URL; preserve query strings and fragments. Report missing targets
|
|
172
|
+
instead of pointing everything at the home page. Strip Elementor / Gutenberg / Breakdance class
|
|
157
173
|
soup. Drop empty `<div>` and `<span>` wrappers. Keep semantic tags,
|
|
158
174
|
tables, iframes from known hosts (YouTube / Vimeo / Calendly).
|
|
159
175
|
Remove every `<script>`: plugin and theme JavaScript is never copied.
|
package/skills/tr-new-site.md
CHANGED
|
@@ -165,7 +165,7 @@ it there.
|
|
|
165
165
|
### 3. Header + footer partials
|
|
166
166
|
|
|
167
167
|
**Start from the native preset — don't hand-roll navigation.** Read
|
|
168
|
-
`tr-header-footer` and use its `template/site_logo` + `core/
|
|
168
|
+
`tr-header-footer` and use its `template/site_logo` + `core/navigation_menu` with `core/navigation_links` slots
|
|
169
169
|
composition. The server-rendered landmark, current-page state, no-JS links,
|
|
170
170
|
mobile disclosure, focus treatment, and responsive behavior are Core
|
|
171
171
|
contracts rather than tenant CSS/JavaScript.
|
|
@@ -269,12 +269,10 @@ passing the whole tree inline (`block={ type: 'core/columns', slots:
|
|
|
269
269
|
slot_index=0|1`. Requires template_capabilities_version ≥ 0.15.2 — on
|
|
270
270
|
older sites use `core/grid` (children flow into columns) instead.
|
|
271
271
|
|
|
272
|
-
Partial last rows
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
don't hand-roll 6-column CSS tricks; both distort content to patch
|
|
277
|
-
layout. On older sites, pick a column count that divides N.
|
|
272
|
+
Partial last rows: keep `last_row: 'start'` on `core/grid`. Incomplete rows
|
|
273
|
+
stay left-aligned, preserving the reading edge. Center only after an explicit
|
|
274
|
+
owner design request, never as an automatic fix. Do not widen one peer card
|
|
275
|
+
or invent six-column CSS to fill a gap.
|
|
278
276
|
|
|
279
277
|
Icons (template_capabilities_version ≥ 0.16.0): `type: 'icon'` fields on
|
|
280
278
|
`core/icon`, `core/icon_box`, and `core/step_card` render inline SVG when
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Verification
|
|
2
|
+
|
|
3
|
+
- Confirm Organization, Site, Version and deployed/source identity.
|
|
4
|
+
- Run `python3 scripts/typeroll/check_unsaved.py` before trigger_deploy. Review
|
|
5
|
+
and explicitly save authorized Pages and partials; templates save immediately.
|
|
6
|
+
- Compare preserved text with capture_text.cjs + compare_text.py and an explicit
|
|
7
|
+
selector. Check intentional additions, source comments and images separately.
|
|
8
|
+
- Inspect desktop/mobile previews and actual hosted output around breakpoints.
|
|
9
|
+
Use shootframe.cjs / shoot_hosted.cjs and inspect full screenshots, not only tiles.
|
|
10
|
+
- Exercise menus, keyboard focus, current links, forms and enabled app states.
|
|
11
|
+
Use card-stacking-check.cjs for whole-card excerpt/media/whitespace targets.
|
|
12
|
+
- Check actual contrast on light/dark/photo backgrounds, image subjects and
|
|
13
|
+
sharpness, footer balance, left-aligned last rows and heading hierarchy.
|
|
14
|
+
- Check home title/suffix, metadata, canonical/indexing policy and JSON-LD.
|
|
15
|
+
- Run check_url_parity.py and check_assets.py on the deployed candidate; compare
|
|
16
|
+
final targets with the approved URL map and flatten accidental redirect chains.
|
|
17
|
+
- Treat 403/network/DNS failures as unverified. Do not claim search-bot access
|
|
18
|
+
from a user-agent string or turn off customer protection without approval.
|
|
19
|
+
- Record tested pages, viewports, revision, results and unresolved issues per QA
|
|
20
|
+
round. Record product defects in qa/typeroll-platform-feedback.md.
|
|
21
|
+
- Keep screenshots and private runtime reports in ignored qa/reports/.
|
|
22
|
+
- Check publishing readiness and migration launch report. Passing technical
|
|
23
|
+
validation does not prove visual/editorial completeness or grant launch approval.
|
|
24
|
+
|
|
25
|
+
Commands/dependencies/limits: scripts/README.md. Design: workflows/design.md.
|
|
26
|
+
Follow existing owner authorization; test publication, traffic cutover and
|
|
27
|
+
indexing have distinct effects but do not require repeated approval if covered.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
- [ ] Check heading role spacing, slot gap and responsive vertical padding in the browser.
|
|
31
|
+
- [ ] Review global/current navigation colors against the real parent background.
|
|
32
|
+
- [ ] Check intrinsic-width image and full-image hero framing at breakpoint boundaries.
|
|
33
|
+
- [ ] Test click-to-load video with keyboard; no provider request before activation.
|
|
34
|
+
- [ ] Review matched drafts, unpublished targets and redirect chains before launch.
|
|
35
|
+
- [ ] Explicitly save working copies, or obtain approval to publish saved content only.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Platform feedback
|
|
2
|
+
|
|
3
|
+
Record site-specific observations here, without credentials or private exports.
|
|
4
|
+
|
|
5
|
+
| ID | Priority | Surface / version | Observation | Evidence | Workaround | Status |
|
|
6
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
7
|
+
|
|
8
|
+
For each finding record expected versus actual behavior, minimal reproduction,
|
|
9
|
+
Site/Version, page/block locator, relevant schema, tested widths, source revision
|
|
10
|
+
and whether it blocks launch. Distinguish bugs, capabilities, instructions and
|
|
11
|
+
project choices. Link upstream fixes; verify deployed output before closing them.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Repeatable site QA
|
|
2
|
+
|
|
3
|
+
These optional tools supplement MCP and the platform's publication validator.
|
|
4
|
+
They do not publish, change DNS, approve a launch or replace visual review.
|
|
5
|
+
Python tools use the standard library except contact_sheet.py (Pillow). Browser
|
|
6
|
+
tools require Node.js 22+, Playwright and its Chromium browser. From scripts/typeroll:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install --no-save --package-lock=false playwright
|
|
10
|
+
npx playwright install chromium
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run the commands below from the **workspace root**. Inject TYPEROLL_API_KEY through
|
|
14
|
+
your private credential manager. Never put it in a file or command argument.
|
|
15
|
+
trlib.py reads portal, organization_id, site_id and version from typeroll.json,
|
|
16
|
+
checks the connected Site/Organization, discovers the live tool schema, and
|
|
17
|
+
passes the Version where supported. It refuses unversioned operations on a
|
|
18
|
+
non-main binding; use the explicit interactive API workflow for organization or
|
|
19
|
+
shared media operations instead. It is a small hosted MCP helper for API keys,
|
|
20
|
+
not a replacement OAuth client or a new general CRUD CLI. REST is also supported
|
|
21
|
+
by Typeroll if your integration already uses it.
|
|
22
|
+
|
|
23
|
+
## Before publication
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
python3 scripts/typeroll/check_unsaved.py
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Exit 1 lists unsaved Pages **and partials**. Review those changes; save only the
|
|
30
|
+
work authorized for this publication, then run again. Templates save immediately.
|
|
31
|
+
A working-copy preview is not proof of saved output. Never auto-save other work.
|
|
32
|
+
|
|
33
|
+
## After an authorized publication
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
python3 scripts/typeroll/check_url_parity.py https://YOUR-TEST-HOST
|
|
37
|
+
python3 scripts/typeroll/check_assets.py https://YOUR-TEST-HOST
|
|
38
|
+
node scripts/typeroll/shoot_hosted.cjs https://YOUR-TEST-HOST qa/reports/hosted /,/about/ 390 768 1023 1024 1280
|
|
39
|
+
node scripts/typeroll/card-stacking-check.cjs https://YOUR-TEST-HOST/listing/ 390
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
URL parity reads every migration inventory page and observed spelling, skips
|
|
43
|
+
explicit exclusions unless --show-excluded, keeps origins/query parameters,
|
|
44
|
+
detects loops and flags chains beyond a single redirect (one initial slash
|
|
45
|
+
normalization is allowed). This does **not** prove the final destination is the
|
|
46
|
+
editorially correct page: compare with the approved URL map too. No inventory
|
|
47
|
+
is a failure, not a passing zero-row check. Asset QA uses resolved routes,
|
|
48
|
+
checks published/unlisted Pages, relative URLs, srcset and downloads, uses four
|
|
49
|
+
concurrent HEAD requests and falls back to GET when HEAD is unsupported. CSS
|
|
50
|
+
backgrounds, authenticated assets and dynamically loaded resources need browser QA.
|
|
51
|
+
Neither tool spoofs a search crawler. A 403/DNS/network failure is unverified;
|
|
52
|
+
inspect access policy rather than treating it as a broken migration or a pass.
|
|
53
|
+
|
|
54
|
+
## Other helpers
|
|
55
|
+
|
|
56
|
+
- `call.py TOOL '{"argument":"value"}'`: raw MCP result; use `-` to read JSON
|
|
57
|
+
from stdin. Mutating tools require the same owner authorization as interactive
|
|
58
|
+
calls. Network failures are not automatically retried for mutations.
|
|
59
|
+
- `trlib.upload(path, content_type, alt)`: direct signed upload and mandatory
|
|
60
|
+
finalize SHA-256 verification. No image bytes pass through the model or Git.
|
|
61
|
+
- `sweep_blocks.example.py`: dry-run grid normalization across Pages, templates
|
|
62
|
+
and partials. Review before --apply; refuses to commit existing unsaved work.
|
|
63
|
+
It is not transactional: a failure can leave a partial draft; inspect before retry.
|
|
64
|
+
- `shootframe.cjs OUT-PREFIX SIGNED-PREVIEW-URL [widths...]`: selects only the
|
|
65
|
+
Typeroll preview iframe, reports fonts/hosts/overflow. Signed URLs are secrets;
|
|
66
|
+
pass privately, never paste into committed reports or shell history. Direct-frame
|
|
67
|
+
capture checks rendering only; test Extension storage/navigation/handoffs in
|
|
68
|
+
the original wrapper because those features depend on its parent bridge.
|
|
69
|
+
- `layout_trace.cjs URL [width] [selector]`: computed spacing of ancestors and
|
|
70
|
+
next section. Works with a direct page or Typeroll preview.
|
|
71
|
+
- `contact_sheet.py OUT-PREFIX COLS TILE-WIDTH PNG...`: Pillow contact sheets.
|
|
72
|
+
It crops tall screenshots; inspect full screenshots too.
|
|
73
|
+
- `capture_text.cjs URL qa/reports/text.json 'main .article-content'` followed by
|
|
74
|
+
`compare_text.py sources/private/page.html qa/reports/text.json --source-title 'Title'`:
|
|
75
|
+
word/punctuation diff. A saved single WP REST record (.json) is another source
|
|
76
|
+
adapter. Select exactly the preserved region, including its template-owned
|
|
77
|
+
title once. Deliberate shared additions need separate acceptance. Comments,
|
|
78
|
+
scripts and styles are excluded from source text. It never edits content.
|
|
79
|
+
|
|
80
|
+
Browser reports exit nonzero for missing images, overflow, unexpected H1 count,
|
|
81
|
+
failed requests or incomplete scrolling. They keep third-party failures visible.
|
|
82
|
+
Network checks retry only a bounded read. All reports, screenshots and source
|
|
83
|
+
exports stay under ignored qa/reports/ or sources/private/. Do not commit signed
|
|
84
|
+
URLs, personal data or customer assets. Adapt scripts deliberately and run tests
|
|
85
|
+
before applying broad changes. The workspace updater preserves your edits.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Isolated QA browser only. Never persist storage state, cookies or signed URLs.
|
|
2
|
+
const { chromium } = require('playwright');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
async function open(page, url) {
|
|
6
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
7
|
+
// Select only Typeroll's own preview, never an arbitrary ad/video iframe.
|
|
8
|
+
const frame = page.locator('iframe[title="Preview"], iframe[title="Site preview"]');
|
|
9
|
+
if (await frame.count() === 1) {
|
|
10
|
+
await page.waitForFunction(() => document.querySelector('iframe[title="Preview"], iframe[title="Site preview"]')?.getAttribute('src'), null, { timeout: 15000 });
|
|
11
|
+
const src = await frame.getAttribute('src');
|
|
12
|
+
if (!src) throw Error('Preview frame has no source');
|
|
13
|
+
const target = new URL(src, page.url());
|
|
14
|
+
if (target.origin !== new URL(page.url()).origin) throw Error('Unexpected preview frame origin');
|
|
15
|
+
await page.goto(target.href, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
16
|
+
}
|
|
17
|
+
await page.evaluate(() => document.fonts.ready);
|
|
18
|
+
}
|
|
19
|
+
async function settle(page) {
|
|
20
|
+
// Bounded for infinite-scroll pages; report coverage rather than hanging.
|
|
21
|
+
for (let y = 0; y < 30000; y += 600) {
|
|
22
|
+
await page.evaluate(y => scrollTo(0, y), y);
|
|
23
|
+
await page.waitForTimeout(100);
|
|
24
|
+
if (y + 900 >= await page.evaluate(() => document.documentElement.scrollHeight)) break;
|
|
25
|
+
}
|
|
26
|
+
await page.evaluate(() => scrollTo(0, 0));
|
|
27
|
+
await page.waitForTimeout(300);
|
|
28
|
+
}
|
|
29
|
+
async function inspect(page) {
|
|
30
|
+
return page.evaluate(() => ({
|
|
31
|
+
overflow: document.documentElement.scrollWidth > innerWidth,
|
|
32
|
+
h1: document.querySelectorAll('h1').length,
|
|
33
|
+
title: document.title,
|
|
34
|
+
robots: document.querySelector('meta[name="robots"]')?.content ?? null,
|
|
35
|
+
fonts: { body: getComputedStyle(document.body).fontFamily, heading: getComputedStyle(document.querySelector('h1') || document.body).fontFamily },
|
|
36
|
+
incompleteImages: [...document.images].filter(i => !i.complete || !i.naturalWidth).map(i => new URL(i.currentSrc || i.src, location.href).pathname),
|
|
37
|
+
scrollCoverageComplete: document.documentElement.scrollHeight <= 30000,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
async function screenshot(page, file) {
|
|
41
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
42
|
+
await page.screenshot({ path: file, fullPage: true, animations: 'disabled' });
|
|
43
|
+
}
|
|
44
|
+
function widths(args) {
|
|
45
|
+
const values = (args.length ? args : ['390', '768', '1023', '1024', '1280']).map(Number);
|
|
46
|
+
if (values.some(w => !Number.isInteger(w) || w < 240 || w > 4000)) throw Error('Invalid viewport width');
|
|
47
|
+
return values;
|
|
48
|
+
}
|
|
49
|
+
function fail() { console.error('QA failed; inspect the target privately. URLs and credentials are withheld.'); process.exitCode = 2; }
|
|
50
|
+
module.exports = { chromium, open, settle, inspect, screenshot, widths, fail };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""One MCP call; output can contain private data. Do not commit it."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import trlib
|
|
6
|
+
|
|
7
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
8
|
+
parser.add_argument('tool')
|
|
9
|
+
parser.add_argument('arguments', nargs='?', default='{}', help='JSON or - for stdin')
|
|
10
|
+
args = parser.parse_args()
|
|
11
|
+
value = json.loads(sys.stdin.read() if args.arguments == '-' else args.arguments)
|
|
12
|
+
print(json.dumps(trlib.call(args.tool, value), ensure_ascii=False, indent=2))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Explicit selector avoids silently excluding the wrong column/sidebar.
|
|
2
|
+
const qa = require('./browser.cjs');
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const [url, out, selector = 'main'] = process.argv.slice(2);
|
|
5
|
+
(async () => {
|
|
6
|
+
const browser = await qa.chromium.launch();
|
|
7
|
+
try {
|
|
8
|
+
const page = await browser.newPage();
|
|
9
|
+
await qa.open(page, url); await qa.settle(page);
|
|
10
|
+
const target = page.locator(selector);
|
|
11
|
+
if (await target.count() !== 1) throw Error('Select exactly one content region');
|
|
12
|
+
const value = await target.evaluate(el => ({ text: el.innerText,
|
|
13
|
+
images: [...el.querySelectorAll('img')].map(i => ({ alt: i.alt, path: new URL(i.currentSrc || i.src, location.href).pathname })) }));
|
|
14
|
+
await fs.mkdir(require('node:path').dirname(out), { recursive: true });
|
|
15
|
+
await fs.writeFile(out, JSON.stringify(value, null, 2));
|
|
16
|
+
} finally { await browser.close(); }
|
|
17
|
+
})().catch(qa.fail);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Check every whole-card target, including excerpt/media/meta, without navigating.
|
|
2
|
+
const qa = require('./browser.cjs');
|
|
3
|
+
const [url, width = '390'] = process.argv.slice(2);
|
|
4
|
+
(async () => {
|
|
5
|
+
const browser = await qa.chromium.launch();
|
|
6
|
+
try {
|
|
7
|
+
const page = await browser.newPage({ viewport: { width: qa.widths([width])[0], height: 900 } });
|
|
8
|
+
await qa.open(page, url);
|
|
9
|
+
const cards = page.locator('[data-block="post_card"][data-whole="true"]');
|
|
10
|
+
const report = [];
|
|
11
|
+
for (let index = 0; index < await cards.count(); index++) {
|
|
12
|
+
const card = cards.nth(index);
|
|
13
|
+
await card.scrollIntoViewIfNeeded();
|
|
14
|
+
report.push(await card.evaluate(card => {
|
|
15
|
+
const link = card.querySelector('.block-postcard-title > a');
|
|
16
|
+
const targets = [card, ...card.querySelectorAll('.block-postcard-excerpt,.block-postcard-meta,.block-postcard-media')];
|
|
17
|
+
const hits = targets.filter(el => el.getBoundingClientRect().height > 0).map(el => {
|
|
18
|
+
const r = el.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
|
|
19
|
+
return { part: el.className || 'card', inViewport: y >= 0 && y < innerHeight, linked: document.elementFromPoint(x, y)?.closest('a') === link };
|
|
20
|
+
});
|
|
21
|
+
return { block: card.dataset.blockId || card.dataset.bid || null, title: link?.textContent, hits };
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
console.log(JSON.stringify(report, null, 2));
|
|
25
|
+
if (!report.length || report.some(r => r.hits.some(h => !h.inViewport || !h.linked))) process.exitCode = 1;
|
|
26
|
+
} finally { await browser.close(); }
|
|
27
|
+
})().catch(qa.fail);
|