pandorga 1.4.6 → 1.4.7
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/assets/css/ledger/home.css +21 -11
- data/lib/pandorga/version.rb +1 -1
- data/scripts/test/render-parity/blocks/project-specimen.json +5 -3
- data/scripts/test/render-parity/blocks/system-card.json +8 -3
- data/scripts/test/test-studio-api-contract.rb +70 -0
- data/scripts/test/test-studio-pipeline-focus.mjs +308 -0
- data/scripts/validate.sh +5 -0
- data/studio/assets/{index-CMVzh8F1.js → index-BJyN-kB3.js} +98 -98
- data/studio/index.html +1 -1
- data/studio-app/src/main.js +126 -32
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b72923b7c78519c2e27d696f963cf147445dd0d8dd089f90a69cde8c2247acbd
|
|
4
|
+
data.tar.gz: 703d26b69d010bcd5e535eee0dfd64da725887439cd68edb7cd8da827acb69d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a974cbf723b43a543a0af0a489c74c08bac2bddd4b77275baf5e74c106fde22aa69ea4e0085f0a1a71ba9f25514db61d0e6fc96dd9f03f749645e38e93dfd704
|
|
7
|
+
data.tar.gz: 164b159223d96b978e251f271fc8aea4b6535ad3397205efa88860dd1211b31ace3661b3425d6540b72874f1cd8d4348f8480ae305eccce115d45756dd0bfb5b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.7] — 2026-10-07
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Studio keeps the editor focused while the content-pipeline chip is spinning.
|
|
8
|
+
Status polls patch the toolbar chip instead of rebuilding the shell.
|
|
9
|
+
- Home draft cards use 16:9 thumbnails and are 20% shorter on desktop and mobile.
|
|
10
|
+
|
|
3
11
|
## [1.4.6] — 2026-10-07
|
|
4
12
|
|
|
5
13
|
### Fixed
|
data/assets/css/ledger/home.css
CHANGED
|
@@ -845,10 +845,9 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
845
845
|
border-color var(--motion-duration-fast) var(--motion-ease-default);
|
|
846
846
|
}
|
|
847
847
|
|
|
848
|
-
/*
|
|
849
|
-
keeps room for the excerpt (four-up columns were clipping copy to a sliver). */
|
|
848
|
+
/* Shared 16:9 plate. The previous 16/12.6 crop made these thumbs tall. */
|
|
850
849
|
.home-draft-card .ledger-poster {
|
|
851
|
-
aspect-ratio: 16 /
|
|
850
|
+
aspect-ratio: 16 / 9;
|
|
852
851
|
}
|
|
853
852
|
|
|
854
853
|
/* A handful of fixed degrees per grid position — not a JS random() — so the
|
|
@@ -875,8 +874,9 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
875
874
|
}
|
|
876
875
|
|
|
877
876
|
.home-draft-card .ledger-card-body {
|
|
878
|
-
|
|
879
|
-
|
|
877
|
+
/* 20% shorter stack than the previous dropped-sheet body, both breakpoints. */
|
|
878
|
+
padding: calc(var(--space-md) * 0.8);
|
|
879
|
+
gap: calc(var(--space-xs) * 0.8);
|
|
880
880
|
}
|
|
881
881
|
|
|
882
882
|
/* Up from 1rem. The four dropped sheets are the only place on the home page
|
|
@@ -903,19 +903,29 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
903
903
|
`flex: 1` gives the paragraph a definite block size from the flex
|
|
904
904
|
allocation, so line-clamp/overflow clip against that height instead of
|
|
905
905
|
growing past the ellipsis (the old flex:1 + clamp:2 bug that left text
|
|
906
|
-
after "…" and sliced mid-glyph at the card edge).
|
|
907
|
-
min-height ~40% above the old six-line band so short posters still leave
|
|
908
|
-
readable excerpt room. */
|
|
906
|
+
after "…" and sliced mid-glyph at the card edge). */
|
|
909
907
|
flex: 1 1 auto;
|
|
910
|
-
|
|
908
|
+
/* With the 16:9 plate, this band keeps the whole card about 20% shorter
|
|
909
|
+
than the old 16/12.6 poster and 8.4-line excerpt, on narrow and wide grids. */
|
|
910
|
+
min-height: calc(var(--type-subtitle) * var(--leading-body) * 5.33);
|
|
911
911
|
height: 0;
|
|
912
912
|
display: -webkit-box;
|
|
913
|
-
-webkit-line-clamp:
|
|
914
|
-
line-clamp:
|
|
913
|
+
-webkit-line-clamp: 5;
|
|
914
|
+
line-clamp: 5;
|
|
915
915
|
-webkit-box-orient: vertical;
|
|
916
916
|
overflow: hidden;
|
|
917
917
|
}
|
|
918
918
|
|
|
919
|
+
/* Four-up posters were the tall part of the old card. Give the excerpt a
|
|
920
|
+
little of that height back so the wide card is 20% shorter too, not more. */
|
|
921
|
+
@media (min-width: 1024px) {
|
|
922
|
+
.home-draft-card .ledger-card-copy {
|
|
923
|
+
min-height: calc(var(--type-subtitle) * var(--leading-body) * 6.05);
|
|
924
|
+
-webkit-line-clamp: 6;
|
|
925
|
+
line-clamp: 6;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
919
929
|
.home-draft-card .ledger-card-foot .ledger-cta {
|
|
920
930
|
font-size: var(--type-cta);
|
|
921
931
|
}
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"article.home-project-specimen",
|
|
33
33
|
"div.home-specimen-strip",
|
|
34
34
|
"div.ledger-meta",
|
|
35
|
-
"span.ledger-status",
|
|
36
35
|
"div.home-specimen-figure",
|
|
37
36
|
"div.ledger-poster",
|
|
38
37
|
"img",
|
|
@@ -45,13 +44,16 @@
|
|
|
45
44
|
"span.home-specimen-open"
|
|
46
45
|
],
|
|
47
46
|
"forbidden": [
|
|
48
|
-
"projects-plate"
|
|
47
|
+
"projects-plate",
|
|
48
|
+
"span.ledger-status"
|
|
49
|
+
],
|
|
50
|
+
"excludes": [
|
|
51
|
+
">Ongoing<"
|
|
49
52
|
],
|
|
50
53
|
"contains": [
|
|
51
54
|
">#01<",
|
|
52
55
|
">Simulation<",
|
|
53
56
|
">2024<",
|
|
54
|
-
">Ongoing<",
|
|
55
57
|
">ARCO<",
|
|
56
58
|
">Repository<",
|
|
57
59
|
">Article<",
|
|
@@ -27,22 +27,27 @@
|
|
|
27
27
|
"div.ledger-poster",
|
|
28
28
|
"img",
|
|
29
29
|
"div.ledger-meta",
|
|
30
|
-
"span.ledger-status",
|
|
31
30
|
"h3.ledger-card-title",
|
|
32
31
|
"a.ledger-card-stretch",
|
|
33
32
|
"p.ledger-card-copy",
|
|
34
33
|
"div.ledger-chip-row",
|
|
35
34
|
"div.ledger-card-links"
|
|
36
35
|
],
|
|
36
|
+
"forbidden": [
|
|
37
|
+
"span.ledger-status"
|
|
38
|
+
],
|
|
39
|
+
"excludes": [
|
|
40
|
+
">Ongoing<"
|
|
41
|
+
],
|
|
37
42
|
"contains": [
|
|
38
43
|
">Repository<",
|
|
39
44
|
">Article<",
|
|
40
45
|
">Visit<",
|
|
41
46
|
">ARCO<",
|
|
42
|
-
">Simulation"
|
|
47
|
+
">Simulation<"
|
|
43
48
|
],
|
|
44
49
|
"counts": {
|
|
45
|
-
"ledger-meta-sep":
|
|
50
|
+
"ledger-meta-sep": 0,
|
|
46
51
|
"ledger-cta": 3
|
|
47
52
|
}
|
|
48
53
|
}
|
|
@@ -45,6 +45,76 @@ check!(REPO_ROOT.join("studio-app/src/main.js").read.include?("armPipelineWatch"
|
|
|
45
45
|
"SPA must watch the content pipeline after save")
|
|
46
46
|
check!(REPO_ROOT.join("studio-app/src/main.js").read.include?("refreshPipelineStatus"),
|
|
47
47
|
"SPA must hydrate pipeline status on boot")
|
|
48
|
+
|
|
49
|
+
# Pipeline polls used to call renderShell(), which wipes #app and remounts
|
|
50
|
+
# TipTap. That drops editor focus for the whole run.
|
|
51
|
+
def function_source(source, name)
|
|
52
|
+
re = /(?:async\s+)?function\s+#{Regexp.escape(name)}\s*\(/
|
|
53
|
+
m = source.match(re)
|
|
54
|
+
raise "missing function #{name}" unless m
|
|
55
|
+
i = m.end(0)
|
|
56
|
+
paren = 1
|
|
57
|
+
while i < source.length && paren.positive?
|
|
58
|
+
c = source[i]
|
|
59
|
+
if c == "'" || c == '"' || c == "`"
|
|
60
|
+
q = c
|
|
61
|
+
i += 1
|
|
62
|
+
while i < source.length && source[i] != q
|
|
63
|
+
i += source[i] == "\\" ? 2 : 1
|
|
64
|
+
end
|
|
65
|
+
i += 1
|
|
66
|
+
elsif c == "/" && source[i + 1] == "/"
|
|
67
|
+
nl = source.index("\n", i)
|
|
68
|
+
i = nl ? nl + 1 : source.length
|
|
69
|
+
else
|
|
70
|
+
paren += 1 if c == "("
|
|
71
|
+
paren -= 1 if c == ")"
|
|
72
|
+
i += 1
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
i += 1 while i < source.length && source[i] != "{"
|
|
76
|
+
raise "missing body #{name}" if i >= source.length
|
|
77
|
+
depth = 0
|
|
78
|
+
start = i
|
|
79
|
+
while i < source.length
|
|
80
|
+
c = source[i]
|
|
81
|
+
if c == "{"
|
|
82
|
+
depth += 1
|
|
83
|
+
i += 1
|
|
84
|
+
elsif c == "}"
|
|
85
|
+
depth -= 1
|
|
86
|
+
return source[start..i] if depth == 0
|
|
87
|
+
i += 1
|
|
88
|
+
elsif c == "'" || c == '"' || c == "`"
|
|
89
|
+
q = c
|
|
90
|
+
i += 1
|
|
91
|
+
while i < source.length && source[i] != q
|
|
92
|
+
i += source[i] == "\\" ? 2 : 1
|
|
93
|
+
end
|
|
94
|
+
i += 1
|
|
95
|
+
elsif c == "/" && source[i + 1] == "/"
|
|
96
|
+
nl = source.index("\n", i)
|
|
97
|
+
i = nl ? nl + 1 : source.length
|
|
98
|
+
else
|
|
99
|
+
i += 1
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
raise "unclosed function #{name}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
studio_main = REPO_ROOT.join("studio-app/src/main.js").read
|
|
106
|
+
%w[pollPipelineStatus refreshPipelineStatus armPipelineWatch].each do |fn|
|
|
107
|
+
body = function_source(studio_main, fn)
|
|
108
|
+
check!(!body.include?("renderShell"),
|
|
109
|
+
"#{fn} must not rebuild the shell (that drops editor focus while the chip spins)")
|
|
110
|
+
check!(body.include?("syncPipelineChip"),
|
|
111
|
+
"#{fn} must patch the pipeline chip in place")
|
|
112
|
+
end
|
|
113
|
+
reexport = function_source(studio_main, "reexportLocalContent")
|
|
114
|
+
check!(!reexport.include?("renderShell"),
|
|
115
|
+
"local export must not rebuild the shell while the button spins")
|
|
116
|
+
check!(reexport.include?("syncLocalExportButton"),
|
|
117
|
+
"local export must patch the export button in place")
|
|
48
118
|
check!(!REPO_ROOT.join("studio-app/src/main.js").read.match?(/action:\s*"delete"/),
|
|
49
119
|
"toolbar must not include a Delete action")
|
|
50
120
|
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Pipeline status updates must patch the toolbar chip. Rebuilding the shell
|
|
4
|
+
* drops editor focus for the whole run.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import vm from "node:vm";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
13
|
+
const source = fs.readFileSync(path.join(root, "studio-app/src/main.js"), "utf8");
|
|
14
|
+
|
|
15
|
+
function skipString(i) {
|
|
16
|
+
const q = source[i];
|
|
17
|
+
i += 1;
|
|
18
|
+
while (i < source.length && source[i] !== q) {
|
|
19
|
+
if (source[i] === "\\") i += 2;
|
|
20
|
+
else i += 1;
|
|
21
|
+
}
|
|
22
|
+
return i + 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function functionSource(name) {
|
|
26
|
+
const re = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\(`);
|
|
27
|
+
const m = re.exec(source);
|
|
28
|
+
if (!m) throw new Error(`missing function ${name}`);
|
|
29
|
+
let i = m.index + m[0].length;
|
|
30
|
+
let paren = 1;
|
|
31
|
+
while (i < source.length && paren > 0) {
|
|
32
|
+
const c = source[i];
|
|
33
|
+
if (c === "'" || c === '"' || c === "`") i = skipString(i);
|
|
34
|
+
else if (c === "/" && source[i + 1] === "/") {
|
|
35
|
+
const nl = source.indexOf("\n", i);
|
|
36
|
+
i = nl === -1 ? source.length : nl + 1;
|
|
37
|
+
} else {
|
|
38
|
+
if (c === "(") paren += 1;
|
|
39
|
+
else if (c === ")") paren -= 1;
|
|
40
|
+
i += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
while (i < source.length && source[i] !== "{") i += 1;
|
|
44
|
+
if (i >= source.length) throw new Error(`missing body ${name}`);
|
|
45
|
+
let depth = 0;
|
|
46
|
+
while (i < source.length) {
|
|
47
|
+
const c = source[i];
|
|
48
|
+
if (c === "{") {
|
|
49
|
+
depth += 1;
|
|
50
|
+
i += 1;
|
|
51
|
+
} else if (c === "}") {
|
|
52
|
+
depth -= 1;
|
|
53
|
+
if (depth === 0) return source.slice(m.index, i + 1);
|
|
54
|
+
i += 1;
|
|
55
|
+
} else if (c === "'" || c === '"' || c === "`") {
|
|
56
|
+
i = skipString(i);
|
|
57
|
+
} else if (c === "/" && source[i + 1] === "/") {
|
|
58
|
+
const nl = source.indexOf("\n", i);
|
|
59
|
+
i = nl === -1 ? source.length : nl + 1;
|
|
60
|
+
} else {
|
|
61
|
+
i += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`unclosed function ${name}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class MiniNode {
|
|
68
|
+
constructor(tag) {
|
|
69
|
+
this.nodeType = 1;
|
|
70
|
+
this.tagName = String(tag || "").toUpperCase();
|
|
71
|
+
this.childNodes = [];
|
|
72
|
+
this.parentNode = null;
|
|
73
|
+
this.attributes = new Map();
|
|
74
|
+
this.className = "";
|
|
75
|
+
this.disabled = false;
|
|
76
|
+
this.title = "";
|
|
77
|
+
this.value = "";
|
|
78
|
+
this.selectionStart = 0;
|
|
79
|
+
this.selectionEnd = 0;
|
|
80
|
+
this._text = "";
|
|
81
|
+
this.listeners = new Map();
|
|
82
|
+
const node = this;
|
|
83
|
+
this.classList = {
|
|
84
|
+
toggle(name, force) {
|
|
85
|
+
const parts = node.className.split(/\s+/).filter(Boolean);
|
|
86
|
+
const has = parts.includes(name);
|
|
87
|
+
const on = force === undefined ? !has : !!force;
|
|
88
|
+
const next = parts.filter((part) => part !== name);
|
|
89
|
+
if (on) next.push(name);
|
|
90
|
+
node.className = next.join(" ");
|
|
91
|
+
return on;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get textContent() {
|
|
97
|
+
if (this.childNodes.length === 0) return this._text;
|
|
98
|
+
return this.childNodes.map((child) => child.textContent).join("");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
set textContent(value) {
|
|
102
|
+
this._text = value == null ? "" : String(value);
|
|
103
|
+
this.childNodes = [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setAttribute(name, value) {
|
|
107
|
+
this.attributes.set(name, String(value));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getAttribute(name) {
|
|
111
|
+
return this.attributes.has(name) ? this.attributes.get(name) : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
addEventListener(type, fn) {
|
|
115
|
+
this.listeners.set(type, fn);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
append(...kids) {
|
|
119
|
+
for (const kid of kids) {
|
|
120
|
+
if (kid == null || kid === false) continue;
|
|
121
|
+
kid.parentNode = this;
|
|
122
|
+
this.childNodes.push(kid);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
replaceWith(next) {
|
|
127
|
+
const parent = this.parentNode;
|
|
128
|
+
if (!parent) return;
|
|
129
|
+
const idx = parent.childNodes.indexOf(this);
|
|
130
|
+
if (idx < 0) return;
|
|
131
|
+
next.parentNode = parent;
|
|
132
|
+
parent.childNodes[idx] = next;
|
|
133
|
+
this.parentNode = null;
|
|
134
|
+
if (document.activeElement === this) document.activeElement = document.body;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
querySelector(sel) {
|
|
138
|
+
return querySelector(this, sel);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
focus() {
|
|
142
|
+
document.activeElement = this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setSelectionRange(start, end) {
|
|
146
|
+
this.selectionStart = start;
|
|
147
|
+
this.selectionEnd = end;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function matches(node, sel) {
|
|
152
|
+
const attr = sel.match(/^\[([^=]+)='([^']*)'\]$/);
|
|
153
|
+
if (attr) return node.getAttribute(attr[1]) === attr[2];
|
|
154
|
+
if (sel.startsWith(".")) {
|
|
155
|
+
const name = sel.slice(1);
|
|
156
|
+
return node.className.split(/\s+/).includes(name);
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function querySelector(root, sel) {
|
|
162
|
+
for (const child of root.childNodes || []) {
|
|
163
|
+
if (child.nodeType === 1 && matches(child, sel)) return child;
|
|
164
|
+
const found = querySelector(child, sel);
|
|
165
|
+
if (found) return found;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const document = {
|
|
171
|
+
activeElement: null,
|
|
172
|
+
body: null,
|
|
173
|
+
createElement(tag) {
|
|
174
|
+
return new MiniNode(tag);
|
|
175
|
+
},
|
|
176
|
+
querySelector(sel) {
|
|
177
|
+
return querySelector(document.body, sel);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
document.body = new MiniNode("body");
|
|
181
|
+
document.body.parentNode = document;
|
|
182
|
+
|
|
183
|
+
const editor = new MiniNode("textarea");
|
|
184
|
+
editor.value = "keep typing";
|
|
185
|
+
editor.className = "body-editor";
|
|
186
|
+
const chipHost = new MiniNode("div");
|
|
187
|
+
document.body.append(chipHost, editor);
|
|
188
|
+
|
|
189
|
+
const state = {
|
|
190
|
+
pipeline: {
|
|
191
|
+
state: "idle",
|
|
192
|
+
sha: "",
|
|
193
|
+
runUrl: "",
|
|
194
|
+
message: "",
|
|
195
|
+
pollGen: 0,
|
|
196
|
+
},
|
|
197
|
+
reexporting: false,
|
|
198
|
+
error: "",
|
|
199
|
+
errorDetail: "",
|
|
200
|
+
status: "",
|
|
201
|
+
file: "content/collections/articles/hello.md",
|
|
202
|
+
saving: false,
|
|
203
|
+
dirty: false,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
let statusCalls = 0;
|
|
207
|
+
const studioApi = {
|
|
208
|
+
async pipelineStatus() {
|
|
209
|
+
statusCalls += 1;
|
|
210
|
+
if (statusCalls < 3) {
|
|
211
|
+
return {
|
|
212
|
+
state: "running",
|
|
213
|
+
sha: "abc",
|
|
214
|
+
run: { html_url: "https://example.com/actions/runs/9" },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
state: "ok",
|
|
219
|
+
sha: "abc",
|
|
220
|
+
run: { html_url: "https://example.com/actions/runs/9" },
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const context = {
|
|
226
|
+
state,
|
|
227
|
+
document,
|
|
228
|
+
studioApi,
|
|
229
|
+
sleep: async () => {},
|
|
230
|
+
console,
|
|
231
|
+
JSON,
|
|
232
|
+
String,
|
|
233
|
+
window: { open() {}, location: { hostname: "studio.example.com" } },
|
|
234
|
+
};
|
|
235
|
+
vm.createContext(context);
|
|
236
|
+
vm.runInContext(
|
|
237
|
+
[
|
|
238
|
+
functionSource("el"),
|
|
239
|
+
functionSource("pipelineChipMeta"),
|
|
240
|
+
functionSource("pipelineChipViewKey"),
|
|
241
|
+
functionSource("renderPipelineChip"),
|
|
242
|
+
functionSource("syncPipelineChip"),
|
|
243
|
+
functionSource("applyPipelinePayload"),
|
|
244
|
+
functionSource("localExportPresentation"),
|
|
245
|
+
functionSource("syncLocalExportButton"),
|
|
246
|
+
functionSource("pollPipelineStatus"),
|
|
247
|
+
].join("\n"),
|
|
248
|
+
context
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
chipHost.append(vm.runInContext("renderPipelineChip()", context));
|
|
252
|
+
editor.focus();
|
|
253
|
+
editor.setSelectionRange(5, 5);
|
|
254
|
+
const editorAtFocus = document.activeElement;
|
|
255
|
+
|
|
256
|
+
await vm.runInContext(
|
|
257
|
+
`(async () => {
|
|
258
|
+
state.pipeline.pollGen = 1;
|
|
259
|
+
state.pipeline.state = "awaiting_run";
|
|
260
|
+
syncPipelineChip();
|
|
261
|
+
await pollPipelineStatus(1, "abc");
|
|
262
|
+
})()`,
|
|
263
|
+
context
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
assert.equal(document.activeElement, editorAtFocus, "editor focus survives pipeline polls");
|
|
267
|
+
assert.equal(editor.parentNode, document.body, "editor stays mounted");
|
|
268
|
+
assert.equal(editor.value, "keep typing");
|
|
269
|
+
assert.equal(editor.selectionStart, 5);
|
|
270
|
+
assert.equal(state.pipeline.state, "ok");
|
|
271
|
+
|
|
272
|
+
const chip = document.querySelector("[data-studio-action='pipeline-status']");
|
|
273
|
+
assert.ok(chip, "pipeline chip remains");
|
|
274
|
+
assert.match(chip.className, /pipeline-ok/);
|
|
275
|
+
assert.equal(chip.querySelector(".btn-label").textContent, "Live");
|
|
276
|
+
assert.equal(chip.querySelector(".studio-icon-spin"), null);
|
|
277
|
+
assert.equal(chip.disabled, false);
|
|
278
|
+
|
|
279
|
+
editor.focus();
|
|
280
|
+
state.reexporting = true;
|
|
281
|
+
state.pipeline.state = "syncing";
|
|
282
|
+
vm.runInContext("syncLocalExportButton()", context);
|
|
283
|
+
const exportBtn = document.querySelector("[data-studio-action='reexport']");
|
|
284
|
+
assert.equal(exportBtn, null, "production shell has no local export button");
|
|
285
|
+
|
|
286
|
+
const exportHost = new MiniNode("button");
|
|
287
|
+
exportHost.setAttribute("data-studio-action", "reexport");
|
|
288
|
+
exportHost.className = "btn btn-tool btn-reexport";
|
|
289
|
+
const icon = new MiniNode("span");
|
|
290
|
+
icon.className = "material-symbols-outlined";
|
|
291
|
+
icon.textContent = "sync";
|
|
292
|
+
const label = new MiniNode("span");
|
|
293
|
+
label.className = "btn-label";
|
|
294
|
+
label.textContent = "Export";
|
|
295
|
+
exportHost.append(icon, label);
|
|
296
|
+
document.body.append(exportHost);
|
|
297
|
+
|
|
298
|
+
editor.focus();
|
|
299
|
+
vm.runInContext("syncLocalExportButton()", context);
|
|
300
|
+
assert.equal(document.activeElement, editor, "local export spin does not steal editor focus");
|
|
301
|
+
assert.equal(exportHost.disabled, true);
|
|
302
|
+
assert.match(exportHost.className, /pipeline-running/);
|
|
303
|
+
assert.equal(icon.textContent, "progress_activity");
|
|
304
|
+
assert.match(icon.className, /studio-icon-spin/);
|
|
305
|
+
assert.equal(label.textContent, "Exporting…");
|
|
306
|
+
assert.equal(editor.value, "keep typing");
|
|
307
|
+
|
|
308
|
+
console.log("All Studio pipeline focus checks passed.");
|
data/scripts/validate.sh
CHANGED
|
@@ -73,6 +73,11 @@ if ! node scripts/test/test-studio-pipeline-config.mjs; then
|
|
|
73
73
|
failed=1
|
|
74
74
|
fi
|
|
75
75
|
|
|
76
|
+
echo "-- scripts/test/test-studio-pipeline-focus.mjs"
|
|
77
|
+
if ! node scripts/test/test-studio-pipeline-focus.mjs; then
|
|
78
|
+
failed=1
|
|
79
|
+
fi
|
|
80
|
+
|
|
76
81
|
if [[ "$failed" -ne 0 ]]; then
|
|
77
82
|
echo "VALIDATE FAILED"
|
|
78
83
|
exit 1
|