paperlint 13.6.1 → 14.0.0
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/skills/build-label-page/scripts/label-page-core.d.ts +62 -20
- package/dist/skills/build-label-page/scripts/rule-descriptions.d.ts +6 -1
- package/package.json +1 -1
- package/skills/build-label-page/SKILL.md +5 -4
- package/skills/build-label-page/SKILL.md.spec.ts +4 -3
- package/skills/build-label-page/references/checklist.md +6 -6
- package/skills/build-label-page/references/closed-choice-generator.md +38 -16
- package/skills/build-label-page/scripts/label-page-browser.test.ts +109 -14
- package/skills/build-label-page/scripts/label-page-core.js +119 -36
- package/skills/build-label-page/scripts/label-page-core.test.ts +192 -35
- package/skills/build-label-page/scripts/label-page-core.ts +204 -97
- package/skills/build-label-page/scripts/label-page-html.test.ts +41 -56
- package/skills/build-label-page/scripts/label-page.js +11 -6
- package/skills/build-label-page/scripts/label-page.ts +11 -6
- package/skills/build-label-page/scripts/page-template.html +45 -19
- package/skills/build-label-page/scripts/rule-descriptions.js +48 -30
- package/skills/build-label-page/scripts/rule-descriptions.test.ts +58 -12
- package/skills/build-label-page/scripts/rule-descriptions.ts +56 -33
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RuleRef } from "./rule-descriptions.ts";
|
|
1
|
+
import { type Described as RuleText, type RuleRef } from "./rule-descriptions.ts";
|
|
2
2
|
export declare class Refusal extends Error {
|
|
3
3
|
readonly exitCode: number;
|
|
4
4
|
constructor(exitCode: number, message: string);
|
|
@@ -13,8 +13,8 @@ type Json = null | boolean | number | string | readonly Json[] | {
|
|
|
13
13
|
type JsonObject = {
|
|
14
14
|
readonly [key: string]: Json;
|
|
15
15
|
};
|
|
16
|
-
declare const PAGE_FIELDS: readonly ["group", "title", "body", "code", "context"];
|
|
17
|
-
type SchemaNode = {
|
|
16
|
+
declare const PAGE_FIELDS: readonly ["group", "title", "body", "code", "context", "file"];
|
|
17
|
+
type SchemaNode = ({
|
|
18
18
|
kind: "string";
|
|
19
19
|
required: boolean;
|
|
20
20
|
pattern?: string;
|
|
@@ -40,7 +40,16 @@ type SchemaNode = {
|
|
|
40
40
|
required: boolean;
|
|
41
41
|
closed: boolean;
|
|
42
42
|
fields: Readonly<Record<string, SchemaNode>>;
|
|
43
|
+
}) & {
|
|
44
|
+
why?: string;
|
|
43
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* THE PAGE KINDS, and the card fields each requires on every item and practice card (#373). A fix to
|
|
48
|
+
* a card is a field; a kind that needs it makes it required, so a page built without it fails the
|
|
49
|
+
* schema with the field named instead of shipping the old card.
|
|
50
|
+
*/
|
|
51
|
+
type Kind = "approval" | "witness" | "labels";
|
|
52
|
+
/** The declared schema (`schema` prints it): every field, those a kind requires left optional. */
|
|
44
53
|
export declare const CONFIG_SCHEMA: SchemaNode;
|
|
45
54
|
interface Option {
|
|
46
55
|
readonly value: string;
|
|
@@ -58,28 +67,33 @@ interface Question {
|
|
|
58
67
|
interface Unit {
|
|
59
68
|
readonly id: string;
|
|
60
69
|
readonly text: string;
|
|
70
|
+
readonly mark: string;
|
|
61
71
|
readonly part: number;
|
|
62
72
|
readonly of: number;
|
|
63
73
|
}
|
|
64
|
-
|
|
74
|
+
/** What a card shows, on an item and on a practice item alike. */
|
|
75
|
+
type Card = {
|
|
65
76
|
readonly [F in (typeof PAGE_FIELDS)[number]]?: string;
|
|
66
77
|
} & {
|
|
67
|
-
readonly source_id: string;
|
|
68
|
-
readonly group: string;
|
|
69
78
|
readonly body: string;
|
|
70
79
|
readonly rules?: readonly RuleRef[];
|
|
71
80
|
readonly unit?: Unit;
|
|
81
|
+
readonly readings?: {
|
|
82
|
+
readonly a: string;
|
|
83
|
+
readonly b: string;
|
|
84
|
+
};
|
|
72
85
|
};
|
|
73
|
-
|
|
74
|
-
readonly
|
|
75
|
-
readonly
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
type Item = Card & {
|
|
87
|
+
readonly source_id: string;
|
|
88
|
+
readonly group: string;
|
|
89
|
+
};
|
|
90
|
+
type Practice = Card & {
|
|
78
91
|
readonly expected: Readonly<Record<string, string>>;
|
|
79
92
|
readonly why: string;
|
|
80
|
-
}
|
|
93
|
+
};
|
|
81
94
|
interface Config {
|
|
82
95
|
readonly study: string;
|
|
96
|
+
readonly kind: Kind;
|
|
83
97
|
readonly title: string;
|
|
84
98
|
readonly intro?: string;
|
|
85
99
|
readonly seed: number;
|
|
@@ -97,8 +111,8 @@ interface KeyEntry {
|
|
|
97
111
|
readonly group: string;
|
|
98
112
|
readonly on_page: boolean;
|
|
99
113
|
}
|
|
100
|
-
/** Rule → the linter's description, for every rule
|
|
101
|
-
type Described = ReadonlyMap<string,
|
|
114
|
+
/** Rule → the linter's description (and a ruff rule's family), for every rule a card names. */
|
|
115
|
+
type Described = ReadonlyMap<string, RuleText>;
|
|
102
116
|
/** Only what the labeller can answer goes on the page, and only that gets an id, in id order. */
|
|
103
117
|
declare function itemsOnPage(cfg: Config, keyPath: string): {
|
|
104
118
|
items: Item[];
|
|
@@ -123,8 +137,21 @@ declare function payloadOf(cfg: Config, page: ReturnType<typeof itemsOnPage>, de
|
|
|
123
137
|
questions: readonly Question[];
|
|
124
138
|
practice: {
|
|
125
139
|
round: number;
|
|
126
|
-
items:
|
|
127
|
-
|
|
140
|
+
items: ({
|
|
141
|
+
rules?: never;
|
|
142
|
+
id: string;
|
|
143
|
+
expected: Readonly<Record<string, string>>;
|
|
144
|
+
why: string;
|
|
145
|
+
} | {
|
|
146
|
+
rules: {
|
|
147
|
+
description?: string;
|
|
148
|
+
family?: string;
|
|
149
|
+
linter: "eslint" | "ruff";
|
|
150
|
+
id: string;
|
|
151
|
+
}[];
|
|
152
|
+
id: string;
|
|
153
|
+
expected: Readonly<Record<string, string>>;
|
|
154
|
+
why: string;
|
|
128
155
|
})[];
|
|
129
156
|
};
|
|
130
157
|
items: ({
|
|
@@ -132,9 +159,10 @@ declare function payloadOf(cfg: Config, page: ReturnType<typeof itemsOnPage>, de
|
|
|
132
159
|
id: string;
|
|
133
160
|
} | {
|
|
134
161
|
rules: {
|
|
162
|
+
description?: string;
|
|
163
|
+
family?: string;
|
|
135
164
|
linter: "eslint" | "ruff";
|
|
136
165
|
id: string;
|
|
137
|
-
description: string;
|
|
138
166
|
}[];
|
|
139
167
|
id: string;
|
|
140
168
|
})[];
|
|
@@ -162,8 +190,21 @@ export declare function prepareBuild(configPath: string, outDir: string): {
|
|
|
162
190
|
questions: readonly Question[];
|
|
163
191
|
practice: {
|
|
164
192
|
round: number;
|
|
165
|
-
items:
|
|
166
|
-
|
|
193
|
+
items: ({
|
|
194
|
+
rules?: never;
|
|
195
|
+
id: string;
|
|
196
|
+
expected: Readonly<Record<string, string>>;
|
|
197
|
+
why: string;
|
|
198
|
+
} | {
|
|
199
|
+
rules: {
|
|
200
|
+
description?: string;
|
|
201
|
+
family?: string;
|
|
202
|
+
linter: "eslint" | "ruff";
|
|
203
|
+
id: string;
|
|
204
|
+
}[];
|
|
205
|
+
id: string;
|
|
206
|
+
expected: Readonly<Record<string, string>>;
|
|
207
|
+
why: string;
|
|
167
208
|
})[];
|
|
168
209
|
};
|
|
169
210
|
items: ({
|
|
@@ -171,9 +212,10 @@ export declare function prepareBuild(configPath: string, outDir: string): {
|
|
|
171
212
|
id: string;
|
|
172
213
|
} | {
|
|
173
214
|
rules: {
|
|
215
|
+
description?: string;
|
|
216
|
+
family?: string;
|
|
174
217
|
linter: "eslint" | "ruff";
|
|
175
218
|
id: string;
|
|
176
|
-
description: string;
|
|
177
219
|
}[];
|
|
178
220
|
id: string;
|
|
179
221
|
})[];
|
|
@@ -2,6 +2,11 @@ export interface RuleRef {
|
|
|
2
2
|
readonly linter: "eslint" | "ruff";
|
|
3
3
|
readonly id: string;
|
|
4
4
|
}
|
|
5
|
+
/** What the linter says a rule does; a ruff rule also names the family it comes from. */
|
|
6
|
+
export interface Described {
|
|
7
|
+
readonly description: string;
|
|
8
|
+
readonly family?: string;
|
|
9
|
+
}
|
|
5
10
|
/** A process run to its end: the exit status and what it printed. */
|
|
6
11
|
export type RunProcess = (cmd: string, args: readonly string[]) => {
|
|
7
12
|
readonly status: number | null;
|
|
@@ -9,4 +14,4 @@ export type RunProcess = (cmd: string, args: readonly string[]) => {
|
|
|
9
14
|
};
|
|
10
15
|
export declare const runProcess: RunProcess;
|
|
11
16
|
/** The linter's own one-line description of `ref`, or undefined when it has none. */
|
|
12
|
-
export declare function describeRule(ref: RuleRef, fromDir: string, run?: RunProcess):
|
|
17
|
+
export declare function describeRule(ref: RuleRef, fromDir: string, run?: RunProcess): Described | undefined;
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ description: Build a phone labelling page published as a claude.ai Artifact with
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, Artifact, ArtifactData]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:6e828827dc6fcfc1 compiled from skills/build-label-page/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# build-label-page — a page the person labels on a phone, and the labels back in the repo
|
|
10
10
|
|
|
@@ -63,8 +63,8 @@ building. In one line each:
|
|
|
63
63
|
source labels on the page. The generator refuses to build when an item carries a field outside an
|
|
64
64
|
allow-list; a page written by hand checks its items against the same list.
|
|
65
65
|
5. **Ask only what the labeller can answer.** Only the languages and tools they know; one "how well do
|
|
66
|
-
you know it" question per group
|
|
67
|
-
|
|
66
|
+
you know it" question per group when the study filters on it (`"familiarity": true`; left
|
|
67
|
+
out, none is asked, as on an approval page, whose author has not seen those repositories); "clear-cut or real boundary"
|
|
68
68
|
is the labeller's judgement, not the designer's.
|
|
69
69
|
6. **Export into the repository right away**, one JSON per page into the study's `data/` with the
|
|
70
70
|
export date, commit it, and record answers given in chat with `source: "chat"`. If the page fails,
|
|
@@ -104,7 +104,8 @@ building. In one line each:
|
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
It writes `label-page/page.html` and `label-page/key.json` (the only link from `G001` to the
|
|
107
|
-
source id; it never goes on the page).
|
|
107
|
+
source id; it never goes on the page). `kind` (`approval`, `witness`, `labels`) is required,
|
|
108
|
+
and each kind requires its card fields on every item and practice card. Written by hand, the page meets the requirements above.
|
|
108
109
|
4. **Read it as the labeller, then look at it.** Read the page text once as a person who knows
|
|
109
110
|
nothing about the source: if anything on it tells the answer, take it out. Read each question as
|
|
110
111
|
someone who did not design the study: can they tell what each answer means from the question, its
|
|
@@ -74,8 +74,8 @@ building. In one line each:
|
|
|
74
74
|
source labels on the page. The generator refuses to build when an item carries a field outside an
|
|
75
75
|
allow-list; a page written by hand checks its items against the same list.
|
|
76
76
|
5. **Ask only what the labeller can answer.** Only the languages and tools they know; one "how well do
|
|
77
|
-
you know it" question per group
|
|
78
|
-
|
|
77
|
+
you know it" question per group when the study filters on it (\`"familiarity": true\`; left
|
|
78
|
+
out, none is asked, as on an approval page, whose author has not seen those repositories); "clear-cut or real boundary"
|
|
79
79
|
is the labeller's judgement, not the designer's.
|
|
80
80
|
6. **Export into the repository right away**, one JSON per page into the study's \`data/\` with the
|
|
81
81
|
export date, commit it, and record answers given in chat with \`source: "chat"\`. If the page fails,
|
|
@@ -115,7 +115,8 @@ building. In one line each:
|
|
|
115
115
|
\`\`\`
|
|
116
116
|
|
|
117
117
|
It writes \`label-page/page.html\` and \`label-page/key.json\` (the only link from \`G001\` to the
|
|
118
|
-
source id; it never goes on the page).
|
|
118
|
+
source id; it never goes on the page). \`kind\` (\`approval\`, \`witness\`, \`labels\`) is required,
|
|
119
|
+
and each kind requires its card fields on every item and practice card. Written by hand, the page meets the requirements above.
|
|
119
120
|
4. **Read it as the labeller, then look at it.** Read the page text once as a person who knows
|
|
120
121
|
nothing about the source: if anything on it tells the answer, take it out. Read each question as
|
|
121
122
|
someone who did not design the study: can they tell what each answer means from the question, its
|
|
@@ -81,11 +81,11 @@ the text of an allowed field says.
|
|
|
81
81
|
|
|
82
82
|
- Restrict the items to the languages and tools the labeller knows (`labeller_knows`). Items of
|
|
83
83
|
other groups are not put on the page and get no id.
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
the labeller does not know well.
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
- When the study filters on it, ask one question per group, "how well do you know this language /
|
|
85
|
+
tool" (well / somewhat / not really), with `"familiarity": true` in `items.json`: the study can
|
|
86
|
+
then report the labels with and without the groups the labeller does not know well. Left out, no
|
|
87
|
+
page asks it: on an approval page the question is about a drawn unit, not about repositories the
|
|
88
|
+
author has seen, and "how well do you know the projects of this group" there means nothing.
|
|
89
89
|
- The per-item judgement "clear-cut or a real boundary case" is the labeller's. The study design
|
|
90
90
|
does not decide it in advance and does not pre-fill it.
|
|
91
91
|
|
|
@@ -94,7 +94,7 @@ answer is the only way to find it afterwards. And a boundary case is boundary be
|
|
|
94
94
|
person hesitates; a designer who tags it in advance has measured the design, not the item.
|
|
95
95
|
|
|
96
96
|
**Check.** The build prints how many items were left out; the export has a familiarity value for
|
|
97
|
-
every group, or the page was built
|
|
97
|
+
every group, or the page was built without `"familiarity": true` and the export has no groups.
|
|
98
98
|
|
|
99
99
|
## 6. Export into the repository right away
|
|
100
100
|
|
|
@@ -33,24 +33,45 @@ length.
|
|
|
33
33
|
required `example`: a real case of the study, in a line, that takes that answer. `icon` (an
|
|
34
34
|
emoji) is optional. Two labels that read the same, or two equal values, stop the build.
|
|
35
35
|
- **`practice`**: 3 or more real cases whose answer you are sure of, each with `expected` (one answer
|
|
36
|
-
per question) and a one-line `why`. Never an item that is also on the page to be labelled.
|
|
36
|
+
per question) and a one-line `why`. Never an item that is also on the page to be labelled. A
|
|
37
|
+
practice card carries the same fields as the real cards it rehearses (`rules`, `unit`,
|
|
38
|
+
`readings`, `file`).
|
|
37
39
|
- **`practice_round`**: raise it after rewording a question; the practice reopens and the answers of
|
|
38
40
|
the earlier round are kept but not counted.
|
|
39
41
|
- **`items`**: `source_id` (the study's own, never shown), `group`, `body`, and optionally `title`,
|
|
40
|
-
`code`, `context`, `rules`, `unit`. Nothing else: the allow-list is fixed in
|
|
41
|
-
is taken out of the item, not added to the list to make a
|
|
42
|
+
`code`, `context`, `rules`, `unit`, `readings`, `file`. Nothing else: the allow-list is fixed in
|
|
43
|
+
the generator, and a field outside it is taken out of the item, not added to the list to make a
|
|
44
|
+
build pass.
|
|
45
|
+
- **`kind`** (required): what the page asks, and so which card fields every item and practice card
|
|
46
|
+
must carry. A card without one fails the schema, naming the field and why the kind needs it:
|
|
47
|
+
|
|
48
|
+
| kind | the labeller decides | required on every card |
|
|
49
|
+
| ---------- | ---------------------------------------------------- | -------------------------------------------------- |
|
|
50
|
+
| `approval` | keep or drop a drawn requirement | `unit` (the line, the requirement marked), `rules` |
|
|
51
|
+
| `witness` | why two readings of a requirement disagree on a file | `rules`, `readings`, `file` |
|
|
52
|
+
| `labels` | a gold label, a blind review | nothing beyond the item |
|
|
53
|
+
|
|
54
|
+
Nothing reads `context`: it stays free text. A rule, a line or two readings written there show
|
|
55
|
+
none of what their fields show, so on an `approval` or `witness` page they go in the fields.
|
|
56
|
+
|
|
42
57
|
- **`rules`**: `[{linter, id}]` (`linter` is `eslint` or `ruff`) on an item read as a linter rule.
|
|
43
|
-
The card shows "
|
|
44
|
-
lazily…",
|
|
45
|
-
does" paragraph, or the ESLint rule's
|
|
46
|
-
installed where `items.json` is
|
|
58
|
+
The card's "Rule" row shows "ruff `TID253` (flake8-tidy-imports): Checks for module-level
|
|
59
|
+
imports that should instead be imported lazily…", taken at build time from the linter itself:
|
|
60
|
+
`ruff rule <id>`'s "What it does" paragraph and family, or the ESLint rule's
|
|
61
|
+
`meta.docs.description` from the ESLint and plugins installed where `items.json` is (a plugin
|
|
62
|
+
rule's own name may hold a slash: `n/no-unsupported-features/node-builtins`). The build refuses a rule the linter cannot describe; a
|
|
47
63
|
description is never typed into `items.json`.
|
|
48
|
-
- **`unit`**: `{id, text, part, of}` on an item
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
- **`unit`**: `{id, text, mark, part, of}` on an item drawn from a source line: the whole line,
|
|
65
|
+
the requirement's own words in it (`mark`, shown highlighted), and which requirement of the line
|
|
66
|
+
it is ("TypeScript only (no JavaScript); top-level await is allowed" is two). The card's "Line"
|
|
67
|
+
row shows the line with the mark, and "part 1 of 2" when the line holds several. A drawn sample
|
|
68
|
+
may hold one part of a line and not the others; the build refuses a `mark` that is not in the
|
|
69
|
+
line, and parts of one line that disagree (another text or `of`, a part repeated, split across
|
|
70
|
+
groups), naming the item.
|
|
71
|
+
- **`readings`**: `{a, b}` on an item that compares two readings of one requirement: two rows,
|
|
72
|
+
"Reading A" on a solid accent rule and "Reading B" on a dashed one, in the same place on every
|
|
73
|
+
card. **`file`**: the witness file (a path, a link, what each reading did with it), on its own
|
|
74
|
+
"File" row. `id` names one occurrence of the line (a file and line
|
|
54
75
|
number, say) and is never shown: two files holding the same words are two lines, each judged in
|
|
55
76
|
full. Splitting the line is the study's job, before the page.
|
|
56
77
|
- **`seed`**: fixes the order. Rebuilding with the same seed keeps every id, and a new item gets the
|
|
@@ -89,9 +110,10 @@ drop, the drop carrying `ask_why`.
|
|
|
89
110
|
}
|
|
90
111
|
```
|
|
91
112
|
|
|
92
|
-
The page sets `"
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
The page sets `"kind": "approval"`, so every card carries its `unit` and `rules`, and leaves
|
|
114
|
+
`familiarity` out: the author is judging drawn units, not repositories they have
|
|
115
|
+
seen, so no per-group "how well do you know it" question is asked. Only a page that sets
|
|
116
|
+
`"familiarity": true` asks it (and a `familiarity_question` without it is refused).
|
|
95
117
|
|
|
96
118
|
The items are the drawn units verbatim, in a language the author reads. Units in other languages
|
|
97
119
|
are taken out before the draw, as a population rule the study's `plan.json` states ("English-language
|
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
* cannot show: focus, typing, and a page that is left without the box ever losing focus.
|
|
5
5
|
*/
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
type Browser,
|
|
9
|
+
chromium,
|
|
10
|
+
type Locator,
|
|
11
|
+
type Page,
|
|
12
|
+
} from "playwright-core";
|
|
13
|
+
import { afterAll, test } from "vitest";
|
|
9
14
|
import { z } from "zod";
|
|
10
15
|
import { chromiumPath } from "../../../test/chromium.ts";
|
|
11
16
|
import {
|
|
@@ -66,6 +71,20 @@ async function reasonStored(page: Page, text: string): Promise<void> {
|
|
|
66
71
|
assert.equal(await storedReason(page), text);
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
/**
|
|
75
|
+
* One Chromium for the whole file, each test in a context of its own (`newPage` opens one): a launch
|
|
76
|
+
* per test cost about 5 s apiece on the macOS runner, and the file's wall time grew with every test.
|
|
77
|
+
*/
|
|
78
|
+
let launched: Browser | undefined;
|
|
79
|
+
const sharedBrowser = async (): Promise<Browser> =>
|
|
80
|
+
(launched ??= await chromium.launch({
|
|
81
|
+
executablePath: chromiumPath(),
|
|
82
|
+
args: ["--no-sandbox"],
|
|
83
|
+
}));
|
|
84
|
+
afterAll(async () => {
|
|
85
|
+
await launched?.close();
|
|
86
|
+
});
|
|
87
|
+
|
|
69
88
|
/** A browser test launches Chromium and types: far above vitest's 5 s default on a slow runner. */
|
|
70
89
|
const BROWSER_TEST_MS = 60_000;
|
|
71
90
|
|
|
@@ -76,12 +95,9 @@ async function onApprovalPage(
|
|
|
76
95
|
filter: "all" | "next" = "all",
|
|
77
96
|
): Promise<void> {
|
|
78
97
|
const { page: html } = build(setup(name, approval()));
|
|
79
|
-
const browser = await
|
|
80
|
-
|
|
81
|
-
args: ["--no-sandbox"],
|
|
82
|
-
});
|
|
98
|
+
const browser = await sharedBrowser();
|
|
99
|
+
const page = await browser.newPage();
|
|
83
100
|
try {
|
|
84
|
-
const page = await browser.newPage();
|
|
85
101
|
await page.addInitScript(STORE(PRACTICE_DONE, echoMs));
|
|
86
102
|
await page.goto("about:blank");
|
|
87
103
|
await page.setContent(html, { waitUntil: "domcontentloaded" });
|
|
@@ -90,7 +106,7 @@ async function onApprovalPage(
|
|
|
90
106
|
await page.getByRole("button", { name: /Drop/ }).first().click();
|
|
91
107
|
await steps(page);
|
|
92
108
|
} finally {
|
|
93
|
-
await
|
|
109
|
+
await page.context().close();
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
|
|
@@ -163,12 +179,9 @@ test(
|
|
|
163
179
|
"a Keep needs no reason and writes none: the page stays quiet",
|
|
164
180
|
async () => {
|
|
165
181
|
const { page: html } = build(setup("browser-keep", approval()));
|
|
166
|
-
const browser = await
|
|
167
|
-
|
|
168
|
-
args: ["--no-sandbox"],
|
|
169
|
-
});
|
|
182
|
+
const browser = await sharedBrowser();
|
|
183
|
+
const page = await browser.newPage();
|
|
170
184
|
try {
|
|
171
|
-
const page = await browser.newPage();
|
|
172
185
|
await page.addInitScript(STORE(PRACTICE_DONE, 20));
|
|
173
186
|
await page.goto("about:blank");
|
|
174
187
|
await page.setContent(html, { waitUntil: "domcontentloaded" });
|
|
@@ -180,7 +193,89 @@ test(
|
|
|
180
193
|
[{}],
|
|
181
194
|
);
|
|
182
195
|
} finally {
|
|
183
|
-
await
|
|
196
|
+
await page.context().close();
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
BROWSER_TEST_MS,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
/** A two-reading card with a long URL and compact JSON: what pushed the page sideways on a phone. */
|
|
203
|
+
const URL_LONG = `https://github.com/a/b/blob/${"0".repeat(40)}/src/a/long/path/to/witness.ts`;
|
|
204
|
+
const WITNESS = {
|
|
205
|
+
source_id: "s1",
|
|
206
|
+
group: "python",
|
|
207
|
+
body: "Warn for unused variables, except those prefixed with `_`",
|
|
208
|
+
unit: {
|
|
209
|
+
id: "L",
|
|
210
|
+
text: "Warn for unused variables",
|
|
211
|
+
mark: "unused",
|
|
212
|
+
part: 1,
|
|
213
|
+
of: 1,
|
|
214
|
+
},
|
|
215
|
+
rules: [{ linter: "eslint", id: "no-console" }],
|
|
216
|
+
readings: {
|
|
217
|
+
a: `[{"argsIgnorePattern":"^_","varsIgnorePattern":"^_","caughtErrors":"all"}]`,
|
|
218
|
+
b: "[]",
|
|
219
|
+
},
|
|
220
|
+
file: URL_LONG,
|
|
221
|
+
context: URL_LONG,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/** Where the line, the rule, reading A, reading B and the question sit on a card, top to bottom. */
|
|
225
|
+
const blocksOf = async (card: Locator) =>
|
|
226
|
+
Promise.all(
|
|
227
|
+
[
|
|
228
|
+
".quote",
|
|
229
|
+
".row:has(.lbl:text-is('Rule'))",
|
|
230
|
+
".reading.a",
|
|
231
|
+
".reading.b",
|
|
232
|
+
".ask",
|
|
233
|
+
].map(async (sel) => card.locator(sel).first().boundingBox()),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
test(
|
|
237
|
+
"🔴 at phone width the card reads by position: line, rule, reading A above reading B, then the question; nothing pushes the page sideways",
|
|
238
|
+
async () => {
|
|
239
|
+
const { page: html } = build(
|
|
240
|
+
setup("browser-layout", approval({ items: [WITNESS] })),
|
|
241
|
+
);
|
|
242
|
+
const page = await (
|
|
243
|
+
await sharedBrowser()
|
|
244
|
+
).newPage({ viewport: { width: 400, height: 900 } });
|
|
245
|
+
try {
|
|
246
|
+
await page.addInitScript(STORE(PRACTICE_DONE, 20));
|
|
247
|
+
await page.setContent(html, { waitUntil: "domcontentloaded" });
|
|
248
|
+
const card = page.locator("article.item").last();
|
|
249
|
+
const boxes = await blocksOf(card);
|
|
250
|
+
const [line, rule, a, b, ask] = boxes;
|
|
251
|
+
assert.ok(line && rule && a && b && ask, "every block is on the card");
|
|
252
|
+
// Guards: each block below the one before, A and B in one column, a gap before the question.
|
|
253
|
+
assert.deepEqual(
|
|
254
|
+
boxes
|
|
255
|
+
.slice(1)
|
|
256
|
+
.map(
|
|
257
|
+
(x, i) =>
|
|
258
|
+
(x?.y ?? 0) >= (boxes[i]?.y ?? 0) + (boxes[i]?.height ?? 0),
|
|
259
|
+
),
|
|
260
|
+
[true, true, true, true],
|
|
261
|
+
);
|
|
262
|
+
assert.equal(a.x, b.x);
|
|
263
|
+
assert.ok(ask.y - (b.y + b.height) >= 16, "space before the question");
|
|
264
|
+
assert.deepEqual(
|
|
265
|
+
await card
|
|
266
|
+
.locator(".reading")
|
|
267
|
+
.evaluateAll((els) =>
|
|
268
|
+
els.map((e) => getComputedStyle(e).borderLeftStyle),
|
|
269
|
+
),
|
|
270
|
+
["solid", "dashed"],
|
|
271
|
+
);
|
|
272
|
+
// Guards: a long URL or compact JSON wraps; the page itself never scrolls sideways.
|
|
273
|
+
assert.equal(
|
|
274
|
+
await page.evaluate(() => document.documentElement.scrollWidth),
|
|
275
|
+
400,
|
|
276
|
+
);
|
|
277
|
+
} finally {
|
|
278
|
+
await page.context().close();
|
|
184
279
|
}
|
|
185
280
|
},
|
|
186
281
|
BROWSER_TEST_MS,
|