@bongos/core 1.19.1081 → 1.20.2
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/.bongos-core.json +528 -193
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +32 -0
- package/clients/bongos-client/index.cjs +32 -0
- package/clients/bongos-client/index.d.ts +51 -6
- package/clients/bongos-client/index.mjs +32 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +851 -19
- package/docs/api-reference.md +33 -9
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1666 -1558
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +44 -1
- package/modules/provisioning/routes/render-standup.js +81 -6
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +169 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +62 -31
- package/scripts/gds/provision-teardown.js +7 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +10 -10
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +19 -0
- package/scripts/gds/render-payload.js +39 -65
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +121 -28
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +126 -6
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/rot_inbox.mjs +8 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- package/tests/upgrade_persist_pin.mjs +24 -0
|
@@ -26,6 +26,7 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
26
26
|
const PUB = path.join(ROOT, 'modules/hall-ui/public');
|
|
27
27
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
28
28
|
const lib = require('../modules/hall-ui/public/tweak-editor-lib.js');
|
|
29
|
+
const holes = require('../modules/hall-ui/public/brand-holes.js');
|
|
29
30
|
const pages = require('../modules/copy-desk/pages.js');
|
|
30
31
|
const pageStatus = require('../modules/copy-desk/page-status.js');
|
|
31
32
|
|
|
@@ -69,7 +70,7 @@ const NEXT = {
|
|
|
69
70
|
// ── the mini-DOM over the page's own markup ────────────────────────────────
|
|
70
71
|
const HTML = read('tweak-editor.html');
|
|
71
72
|
const BODY = /<body[^>]*>([\s\S]*)<\/body>/.exec(HTML)[1].replace(/<script[\s\S]*?<\/script>/g, '');
|
|
72
|
-
const SOURCES = ['dom-utils.js', 'tweak-editor-lib.js', 'tweak-editor.js'].map((f) => [f, read(f)]);
|
|
73
|
+
const SOURCES = ['dom-utils.js', 'brand-holes.js', 'tweak-editor-lib.js', 'tweak-editor.js'].map((f) => [f, read(f)]);
|
|
73
74
|
|
|
74
75
|
function augment(el) {
|
|
75
76
|
if (el._aug) return el;
|
|
@@ -95,12 +96,13 @@ function augment(el) {
|
|
|
95
96
|
el.scrollHeight = 40;
|
|
96
97
|
el.inert = false;
|
|
97
98
|
el.focus = () => { boot.focused = el; };
|
|
99
|
+
el.scrollIntoView = () => {};
|
|
98
100
|
el.click = () => { el.clicked = (el.clicked || 0) + 1; };
|
|
99
101
|
return el;
|
|
100
102
|
}
|
|
101
103
|
let boot = null;
|
|
102
104
|
|
|
103
|
-
function bootEditor({ search = '?page=landing:index', answers = {} } = {}) {
|
|
105
|
+
function bootEditor({ search = '?page=landing:index', answers = {}, stored = {} } = {}) {
|
|
104
106
|
const root = augment(makeEl('body', {}));
|
|
105
107
|
root.innerHTML = BODY;
|
|
106
108
|
const byId = (id) => walk(root, (c) => (c.id === id ? c : null));
|
|
@@ -109,6 +111,7 @@ function bootEditor({ search = '?page=landing:index', answers = {} } = {}) {
|
|
|
109
111
|
let timerId = 0;
|
|
110
112
|
const requests = [];
|
|
111
113
|
const fetches = [];
|
|
114
|
+
const store = { ...stored };
|
|
112
115
|
const documentObj = {
|
|
113
116
|
title: 'Tweak a page — Cloud Bongos',
|
|
114
117
|
body: root,
|
|
@@ -130,30 +133,35 @@ function bootEditor({ search = '?page=landing:index', answers = {} } = {}) {
|
|
|
130
133
|
},
|
|
131
134
|
};
|
|
132
135
|
const windowObj = {
|
|
133
|
-
__BRANDING__: {}, __MODULES__: { enabled: {} },
|
|
136
|
+
__BRANDING__: { identity: { productName: 'Cloud Bongos' } }, __MODULES__: { enabled: {} },
|
|
134
137
|
BongosClient: { createClient: () => client },
|
|
135
|
-
location: { host: 'builders.cloudbongos.com', hostname: 'builders.cloudbongos.com', protocol: 'https:', search, pathname: '/builders/tweak-editor', assign: (u) => { out.navigated = u; } },
|
|
138
|
+
location: { host: 'builders.cloudbongos.com', hostname: 'builders.cloudbongos.com', protocol: 'https:', origin: 'https://builders.cloudbongos.com', href: 'https://builders.cloudbongos.com/builders/tweak-editor', search, pathname: '/builders/tweak-editor', assign: (u) => { out.navigated = u; } },
|
|
136
139
|
history: { replaceState: (_, __, u) => { out.replaced = u; } },
|
|
137
|
-
localStorage: { getItem: () => 'off', setItem() {} },
|
|
140
|
+
localStorage: { getItem: (k) => (String(k).startsWith('hall-tweak') ? (store[k] === undefined ? null : store[k]) : 'off'), setItem: (k, v) => { store[k] = v; } },
|
|
138
141
|
addEventListener() {},
|
|
142
|
+
innerHeight: 1000,
|
|
143
|
+
screen: { availWidth: 1600, availHeight: 1000, availLeft: 0, availTop: 0 },
|
|
144
|
+
open: (...a) => { out.opened.push(a); return {}; },
|
|
139
145
|
setTimeout: (fn, ms) => { const id = ++timerId; timers.push({ id, fn, ms }); return id; },
|
|
140
146
|
clearTimeout: (id) => { const i = timers.findIndex((t) => t.id === id); if (i >= 0) timers.splice(i, 1); },
|
|
141
147
|
};
|
|
142
148
|
const sandbox = {
|
|
143
149
|
window: windowObj, document: documentObj, location: windowObj.location,
|
|
144
150
|
fetch: async (url, init) => { fetches.push({ url, init }); const a = answers[`FETCH ${init.method} ${url.replace(/^\/api\/bongos/, '')}`]; return { ok: !!(a && a.ok), json: async () => (a ? a.data : null) }; },
|
|
145
|
-
console, URLSearchParams, setTimeout, clearTimeout,
|
|
151
|
+
console, URL, URLSearchParams, setTimeout, clearTimeout,
|
|
146
152
|
};
|
|
147
153
|
sandbox.globalThis = sandbox;
|
|
148
154
|
vm.createContext(sandbox);
|
|
149
155
|
for (const [name, src] of SOURCES) vm.runInContext(src, sandbox, { filename: name });
|
|
150
156
|
|
|
151
|
-
const fire = (type, target) => (docListeners[type] || []).forEach((fn) => fn({ type, target }));
|
|
157
|
+
const fire = (type, target, extra) => (docListeners[type] || []).forEach((fn) => fn({ type, target, ...(extra || {}) }));
|
|
152
158
|
const out = {
|
|
153
|
-
requests, fetches, navigated: null, replaced: null, focused: null,
|
|
159
|
+
requests, fetches, store, opened: [], navigated: null, replaced: null, focused: null,
|
|
154
160
|
el: byId,
|
|
155
161
|
window: windowObj,
|
|
156
162
|
lines: () => root.querySelectorAll('textarea'),
|
|
163
|
+
fire: (type, target, extra) => fire(type, target, extra),
|
|
164
|
+
key: (id, keyName, shiftKey) => { const el = byId(id); const ev = { key: keyName, shiftKey: !!shiftKey, prevented: false, preventDefault() { ev.prevented = true; } }; fire('keydown', el, ev); return ev; },
|
|
157
165
|
type: (key, value) => { const ta = byId(`tw-${key}`); ta.value = value; fire('input', ta); },
|
|
158
166
|
click: async (id) => { const el = byId(id); assert.ok(el, `no #${id}`); fire('click', el); await settle(); await settle(); },
|
|
159
167
|
clickEl: async (el) => { fire('click', el); await settle(); await settle(); },
|
|
@@ -207,6 +215,18 @@ try {
|
|
|
207
215
|
assert.equal(lib.uploadedLabel({ from_file: ['L1', 'L2', 'L3'] }), 'uploaded · 3 lines from your doc');
|
|
208
216
|
});
|
|
209
217
|
|
|
218
|
+
await test('the check: next line, counts only lines on the page, remembered per page and reading', () => {
|
|
219
|
+
assert.equal(lib.nextLineKey(READING.lines, 'L0003'), 'L0004');
|
|
220
|
+
assert.equal(lib.nextLineKey(READING.lines, 'L0007'), null, 'the last line has no next');
|
|
221
|
+
assert.equal(lib.nextLineKey(READING.lines, 'L9999'), null);
|
|
222
|
+
assert.equal(lib.confirmedCount(READING.lines, new Set(['L0001', 'L0004', 'L9999'])), 2);
|
|
223
|
+
assert.equal(lib.confirmStoreKey('landing:index', 'rh-1'), 'hall-tweak-checked:landing:index:rh-1');
|
|
224
|
+
assert.deepEqual([...lib.parseConfirmed('["L0002","L9999"]', READING.lines)], ['L0002'], 'a key the reading no longer has is dropped');
|
|
225
|
+
assert.deepEqual([...lib.parseConfirmed('off', READING.lines)], [], 'junk is nothing checked');
|
|
226
|
+
assert.deepEqual([...lib.parseConfirmed(null, READING.lines)], []);
|
|
227
|
+
assert.match(lib.hintText(3, 7), /^Shift \+ Enter checks a line and jumps to the next one\..* 3 of 7 checked\.$/);
|
|
228
|
+
});
|
|
229
|
+
|
|
210
230
|
// ── the page, booted ─────────────────────────────────────────────────────
|
|
211
231
|
await test('the page opens on the read: every line in page order, the saved draft in its box, notes and the accent mark', async () => {
|
|
212
232
|
const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
|
|
@@ -223,7 +243,7 @@ try {
|
|
|
223
243
|
assert.equal(b.el('tw-name').textContent, 'Home');
|
|
224
244
|
assert.equal(b.el('tw-surface').textContent, '· landing');
|
|
225
245
|
assert.equal(b.el('tw-title').textContent, 'Home');
|
|
226
|
-
assert.equal(b.el('tw-open').href, 'https://cloudbongos.com/');
|
|
246
|
+
assert.equal(b.el('tw-open-tab').href, 'https://cloudbongos.com/');
|
|
227
247
|
assert.equal(b.el('tw-submit').disabled, false);
|
|
228
248
|
assert.equal(b.lines().some((t) => t.readOnly), false);
|
|
229
249
|
// Section headings, one per consecutive run.
|
|
@@ -453,14 +473,171 @@ try {
|
|
|
453
473
|
assert.equal(b.lines().length, 7);
|
|
454
474
|
});
|
|
455
475
|
|
|
476
|
+
await test('the check button greens a line, is remembered, and toggles back', async () => {
|
|
477
|
+
const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
|
|
478
|
+
await b.settle();
|
|
479
|
+
assert.equal(b.el('tw-hint').hidden, false, 'the hint teaches Shift + Enter');
|
|
480
|
+
assert.match(b.el('tw-hint').textContent, /Shift \+ Enter .* 0 of 7 checked\./);
|
|
481
|
+
const btn = b.el('tw-doc').querySelector('[data-check="L0003"]');
|
|
482
|
+
await b.clickEl(btn);
|
|
483
|
+
const row = b.el('tw-L0003').closest('.tw-row');
|
|
484
|
+
assert.ok(row.classList.contains('tw-row--checked'));
|
|
485
|
+
assert.equal(btn.getAttribute('aria-pressed'), 'true');
|
|
486
|
+
assert.match(b.el('tw-hint').textContent, /1 of 7 checked\./);
|
|
487
|
+
assert.equal(b.store['hall-tweak-checked:landing:index:rh-1'], '["L0003"]', 'kept in the browser, per page and reading');
|
|
488
|
+
assert.equal(b.requests.filter((r) => r.method !== 'GET').length, 0, 'never sent to the server');
|
|
489
|
+
await b.clickEl(btn);
|
|
490
|
+
assert.equal(row.classList.contains('tw-row--checked'), false);
|
|
491
|
+
assert.equal(b.store['hall-tweak-checked:landing:index:rh-1'], '[]');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
await test('Shift + Enter checks the line and lands in the next; Enter alone does nothing', async () => {
|
|
495
|
+
const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
|
|
496
|
+
await b.settle();
|
|
497
|
+
const plain = b.key('tw-L0003', 'Enter', false);
|
|
498
|
+
assert.equal(plain.prevented, false, 'a plain Enter is left alone');
|
|
499
|
+
assert.equal(b.el('tw-L0003').closest('.tw-row').classList.contains('tw-row--checked'), false);
|
|
500
|
+
const ev = b.key('tw-L0003', 'Enter', true);
|
|
501
|
+
assert.equal(ev.prevented, true, 'no new line is typed');
|
|
502
|
+
assert.ok(b.el('tw-L0003').closest('.tw-row').classList.contains('tw-row--checked'));
|
|
503
|
+
assert.equal(boot.focused && boot.focused.id, 'tw-L0004', 'the cursor moves to the next line');
|
|
504
|
+
b.key('tw-L0007', 'Enter', true);
|
|
505
|
+
assert.ok(b.el('tw-L0007').closest('.tw-row').classList.contains('tw-row--checked'), 'the last line checks too');
|
|
506
|
+
assert.match(b.el('tw-hint').textContent, /2 of 7 checked\./);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
await test('restore puts the page words back, takes the check off, and only shows on a changed line', async () => {
|
|
510
|
+
const puts = [];
|
|
511
|
+
const b = bootEditor({ answers: {
|
|
512
|
+
'GET /copy-desk/pages/landing:index/draft': ok(editorRead()),
|
|
513
|
+
'PUT /copy-desk/pages/landing:index/draft': (body) => { puts.push(body); return ok({ ok: true, outcome: 'saved' }); },
|
|
514
|
+
} });
|
|
515
|
+
await b.settle();
|
|
516
|
+
const doc = b.el('tw-doc');
|
|
517
|
+
assert.equal(doc.querySelector('[data-restore="L0004"]').hidden, false, 'the changed line offers restore');
|
|
518
|
+
assert.equal(doc.querySelector('[data-restore="L0003"]').hidden, true, 'an untouched line does not');
|
|
519
|
+
b.key('tw-L0004', 'Enter', true);
|
|
520
|
+
assert.ok(b.el('tw-L0004').closest('.tw-row').classList.contains('tw-row--checked'));
|
|
521
|
+
await b.clickEl(doc.querySelector('[data-restore="L0004"]'));
|
|
522
|
+
assert.equal(b.el('tw-L0004').value, 'AI agents claim tasks and ship the code.');
|
|
523
|
+
assert.equal(b.el('tw-L0004').closest('.tw-row').classList.contains('tw-row--checked'), false, 'restore ungreens the line');
|
|
524
|
+
assert.equal(b.el('tw-L0004').classList.contains('tw-line--changed'), false);
|
|
525
|
+
assert.equal(doc.querySelector('[data-restore="L0004"]').hidden, true);
|
|
526
|
+
assert.equal(b.el('tw-counter').textContent, '0 of 7 rewritten · saved');
|
|
527
|
+
await b.flushTimers();
|
|
528
|
+
assert.equal(JSON.stringify(puts), JSON.stringify([{ reading_hash: 'rh-1', lines: [] }]), 'the draft saves without it');
|
|
529
|
+
b.type('L0003', 'Building.');
|
|
530
|
+
assert.equal(doc.querySelector('[data-restore="L0003"]').hidden, false, 'typing over a line brings restore back');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
await test('a page you cannot write has no checks, no restore and no hint', async () => {
|
|
534
|
+
const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead({ held: false, state: 'ready' })) } });
|
|
535
|
+
await b.settle();
|
|
536
|
+
assert.equal(b.el('tw-hint').hidden, true);
|
|
537
|
+
assert.equal(b.el('tw-doc').querySelectorAll('button').length, 0);
|
|
538
|
+
const ev = b.key('tw-L0003', 'Enter', true);
|
|
539
|
+
assert.equal(ev.prevented, false);
|
|
540
|
+
});
|
|
541
|
+
|
|
456
542
|
// ── the page's contract with the hall ────────────────────────────────────
|
|
543
|
+
// ── task 1004381: a brand hole is a chip the artist types around ────────────
|
|
544
|
+
const HOLED = {
|
|
545
|
+
id: 'landing:index',
|
|
546
|
+
reading_hash: 'rh-2',
|
|
547
|
+
lines: [
|
|
548
|
+
L('L0001', 'contact', 'Both are how {{productName}} itself works.', 'placed', { file: 'modules/public-landing/public/contact.html', line: 4, string_id: 'c1' }),
|
|
549
|
+
L('L0002', 'contact', 'Plain words.', 'placed', { file: 'modules/public-landing/public/contact.html', line: 5, string_id: 'c2' }),
|
|
550
|
+
],
|
|
551
|
+
};
|
|
552
|
+
const holedRead = ({ held = true } = {}) => pageStatus.composeEditor({
|
|
553
|
+
page: PAGE,
|
|
554
|
+
readingPage: HOLED,
|
|
555
|
+
round: { id: 56, status: 'active', holder_id: held ? ME : null, holder_login: held ? 'me' : null, holder_worktree: false, description: pages.roundTaskBody(PAGE, 1) },
|
|
556
|
+
builderId: ME,
|
|
557
|
+
});
|
|
558
|
+
// DOM-shaped children of a chip box, as the browser holds them.
|
|
559
|
+
const txt = (s) => ({ nodeType: 3, nodeValue: s });
|
|
560
|
+
const chip = (name) => ({ nodeType: 1, nodeName: 'SPAN', getAttribute: (k) => (k === 'data-hole' ? name : null) });
|
|
561
|
+
|
|
562
|
+
await test('brand holes: split, labelled from the pack, compared, and read back from a chip box', () => {
|
|
563
|
+
const brand = { identity: { productName: 'Cloud Bongos', worldName: '' }, currency: { label: 'credits' } };
|
|
564
|
+
assert.deepEqual(holes.splitHoles('a {{productName}} b'), [{ text: 'a ' }, { hole: 'productName' }, { text: ' b' }]);
|
|
565
|
+
assert.equal(holes.holeLabel('productName', brand), 'Cloud Bongos');
|
|
566
|
+
assert.equal(holes.holeLabel('Currency', brand), 'Credits');
|
|
567
|
+
assert.equal(holes.holeLabel('worldName', brand), 'worldName', 'an empty pack value names the hole rather than showing nothing');
|
|
568
|
+
assert.equal(holes.sameHoles('x {{productName}} y', '{{productName}}z'), true);
|
|
569
|
+
assert.equal(holes.sameHoles('x {{productName}} y', 'x Cloud Bongos y'), false, 'a dropped hole is not the same');
|
|
570
|
+
assert.equal(holes.sameHoles('{{a}}', '{{a}} {{a}}'), false, 'nor is an added one');
|
|
571
|
+
assert.equal(holes.holesHtml('<b>{{productName}}</b>', { brand, cls: 'tw-hole', inBox: true }),
|
|
572
|
+
'<b><span class="tw-hole" data-hole="productName" contenteditable="false" title="A fixed name the page fills in: {{productName}}">Cloud Bongos</span></b>');
|
|
573
|
+
assert.equal(holes.serializeHoles([txt('This project, '), chip('productName'), txt(' has '), { nodeType: 1, nodeName: 'BR', getAttribute: () => null }, txt('two.')]),
|
|
574
|
+
'This project, {{productName}} has two.', 'every typed space is kept, a chip is its token again');
|
|
575
|
+
// A chip that ends the box gets a zero-width caret stop after it (a browser
|
|
576
|
+
// cannot put the caret after a trailing non-editable chip); it never saves.
|
|
577
|
+
const ZW = String.fromCharCode(0x200b);
|
|
578
|
+
assert.ok(holes.holesHtml('Hi {{productName}}', { brand, inBox: true }).endsWith(`</span>${ZW}`));
|
|
579
|
+
assert.ok(holes.holesHtml('{{productName}} hi', { brand, inBox: true }).startsWith(`${ZW}<span`));
|
|
580
|
+
assert.equal(holes.holesHtml('Hi {{productName}}', { brand }).includes(ZW), false, 'display outside a box carries none');
|
|
581
|
+
assert.equal(holes.serializeHoles([txt(ZW), chip('productName'), txt(`${ZW} today`)]), '{{productName}} today');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
await test('a line holding {{productName}} is a box with the product name as a fixed chip, and saves the token', async () => {
|
|
585
|
+
const puts = [];
|
|
586
|
+
const b = bootEditor({ answers: {
|
|
587
|
+
'GET /copy-desk/pages/landing:index/draft': ok(holedRead()),
|
|
588
|
+
'PUT /copy-desk/pages/landing:index/draft': (body) => { puts.push(body); return ok({ ok: true, outcome: 'saved' }); },
|
|
589
|
+
} });
|
|
590
|
+
await b.settle();
|
|
591
|
+
const box = b.el('tw-L0001');
|
|
592
|
+
assert.equal(box.tagName, 'div', 'a holed line is not a textarea');
|
|
593
|
+
assert.equal(box.attrs.contenteditable, 'true');
|
|
594
|
+
assert.equal(box.attrs.role, 'textbox');
|
|
595
|
+
const c = box.querySelector('.tw-hole');
|
|
596
|
+
assert.ok(c, 'the hole is a chip');
|
|
597
|
+
assert.equal(c.attrs['data-hole'], 'productName');
|
|
598
|
+
assert.equal(c.attrs.contenteditable, 'false', 'the chip cannot be typed into');
|
|
599
|
+
assert.equal(c.textContent, 'Cloud Bongos', 'it reads as the product name');
|
|
600
|
+
assert.ok(!/\{\{/.test(box.innerHTML.replace(/title="[^"]*"/g, '')), 'no raw token in the box');
|
|
601
|
+
assert.equal(b.el('tw-L0002').tagName, 'textarea', 'a line with no hole is untouched');
|
|
602
|
+
|
|
603
|
+
box.childNodes = [txt('This project, '), chip('productName'), txt(', has two doors.')];
|
|
604
|
+
b.fire("input", box);
|
|
605
|
+
assert.match(b.el('tw-L0001-note').innerHTML, /was: Both are how <span class="tw-hole" data-hole="productName"[^>]*>Cloud Bongos<\/span> itself works\./, 'the was: note shows the chip too');
|
|
606
|
+
await b.flushTimers();
|
|
607
|
+
assert.equal(JSON.stringify(puts), JSON.stringify([{ reading_hash: 'rh-2', lines: [{ key: 'L0001', after: 'This project, {{productName}}, has two doors.' }] }]),
|
|
608
|
+
'the stored words keep the {{token}} and every space around it');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
await test('deleting or adding a hole is undone on the spot and saves nothing; read-only boxes are not editable', async () => {
|
|
612
|
+
const puts = [];
|
|
613
|
+
const b = bootEditor({ answers: {
|
|
614
|
+
'GET /copy-desk/pages/landing:index/draft': ok(holedRead()),
|
|
615
|
+
'PUT /copy-desk/pages/landing:index/draft': (body) => { puts.push(body); return ok({ ok: true, outcome: 'saved' }); },
|
|
616
|
+
} });
|
|
617
|
+
await b.settle();
|
|
618
|
+
const box = b.el('tw-L0001');
|
|
619
|
+
box.childNodes = [txt('Both are how Cloud Bongos itself works.')];
|
|
620
|
+
b.fire("input", box);
|
|
621
|
+
assert.equal(b.pending(), 0, 'no save is scheduled');
|
|
622
|
+
assert.ok(box.querySelector('.tw-hole'), 'the chip is put back');
|
|
623
|
+
box.childNodes = [txt('Both '), chip('productName'), chip('productName')];
|
|
624
|
+
b.fire("input", box);
|
|
625
|
+
assert.equal(b.pending(), 0);
|
|
626
|
+
await b.flushTimers();
|
|
627
|
+
assert.equal(puts.length, 0);
|
|
628
|
+
|
|
629
|
+
const r = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(holedRead({ held: false })) } });
|
|
630
|
+
await r.settle();
|
|
631
|
+
assert.equal(r.el('tw-L0001').attrs.contenteditable, 'false', 'a page you do not hold cannot be typed into');
|
|
632
|
+
});
|
|
633
|
+
|
|
457
634
|
await test('the markup: one h1, the shell\'s full-screen switch, the lib before the page, no web font', () => {
|
|
458
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
459
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
460
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
461
|
-
assert.match(HTML, /shell\.js\?v=2026-09-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-09-29-renderdeploy/);
|
|
462
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
463
|
-
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
640
|
+
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
464
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|
|
465
642
|
}
|
|
466
643
|
assert.match(HTML, />← studio</);
|
|
@@ -468,6 +645,134 @@ try {
|
|
|
468
645
|
assert.match(HTML, />back to the studio</);
|
|
469
646
|
});
|
|
470
647
|
|
|
648
|
+
await test('no visible page title or sub text on the paper; the h1 stays for screen readers', () => {
|
|
649
|
+
assert.match(HTML, /<h1 class="tw-sr" id="tw-title">/);
|
|
650
|
+
assert.equal(/tw-intro|tw-head/.test(HTML), false, 'the intro sentence and its block are gone');
|
|
651
|
+
assert.equal(/Every word on the page, in the order it shows them/.test(HTML), false);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
await test('split view can frame only its own host; the live site refuses to be framed from another', () => {
|
|
655
|
+
const here = { origin: 'https://builders.cloudbongos.com', href: 'https://builders.cloudbongos.com/builders/tweak-editor' };
|
|
656
|
+
assert.equal(lib.canFrame('https://builders.cloudbongos.com/studio', here), true);
|
|
657
|
+
assert.equal(lib.canFrame('https://cloudbongos.com/', here), false, 'the apex is another origin');
|
|
658
|
+
assert.equal(lib.canFrame('not a url', { origin: '', href: '' }), false);
|
|
659
|
+
assert.equal(lib.splitWindowFeatures({ availWidth: 1600, availHeight: 1000, availLeft: 0, availTop: 0 }), 'popup=yes,width=1600,height=500,left=0,top=500', 'the bottom half of the screen');
|
|
660
|
+
assert.match(lib.splitWindowFeatures(undefined), /^popup=yes,width=\d+,height=\d+,left=0,top=\d+$/);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
await test('the split button and the divider: last choice, and a size kept between 20% and 80%', () => {
|
|
664
|
+
assert.equal(lib.openChoice('split'), 'split');
|
|
665
|
+
assert.equal(lib.openChoice('off'), 'tab', 'anything else is a new tab');
|
|
666
|
+
assert.equal(lib.openChoice(null), 'tab');
|
|
667
|
+
assert.equal(lib.OPEN_WORDS.tab, 'open the page ↗');
|
|
668
|
+
assert.equal(lib.OPEN_WORDS.split, 'open in split view');
|
|
669
|
+
assert.equal(lib.clampSplit(null), 0.5);
|
|
670
|
+
assert.equal(lib.clampSplit('off'), 0.5);
|
|
671
|
+
assert.equal(lib.clampSplit('0.95'), 0.8);
|
|
672
|
+
assert.equal(lib.clampSplit(0.05), 0.2);
|
|
673
|
+
assert.equal(lib.clampSplit('0.333'), 0.33);
|
|
674
|
+
assert.equal(lib.splitFromPointer(700, 1000), 0.3, 'the page takes what lies below the pointer');
|
|
675
|
+
assert.equal(lib.splitFromPointer(0, 1000), 0.8);
|
|
676
|
+
assert.equal(lib.splitFromPointer(500, 0), 0.5);
|
|
677
|
+
assert.equal(lib.splitFromKey(0.5, 'ArrowUp'), 0.55);
|
|
678
|
+
assert.equal(lib.splitFromKey(0.5, 'ArrowDown'), 0.45);
|
|
679
|
+
assert.equal(lib.splitFromKey(0.8, 'ArrowUp'), 0.8);
|
|
680
|
+
assert.equal(lib.splitFromKey(0.5, 'Home'), 0.2);
|
|
681
|
+
assert.equal(lib.splitFromKey(0.5, 'End'), 0.8);
|
|
682
|
+
assert.equal(lib.splitFromKey(0.5, 'x'), null);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
await test('open the page: the arrow opens the menu, and the main button repeats the last choice', async () => {
|
|
686
|
+
const saved = PAGE.url;
|
|
687
|
+
PAGE.url = 'https://builders.{apex}/studio';
|
|
688
|
+
try {
|
|
689
|
+
const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
|
|
690
|
+
await b.settle();
|
|
691
|
+
assert.equal(b.el('tw-open').textContent, 'open the page ↗', 'a new tab by default');
|
|
692
|
+
assert.equal(b.el('tw-open-tab').attrs.target, '_blank');
|
|
693
|
+
assert.equal(b.el('tw-frame').hidden, true, 'no frame until asked');
|
|
694
|
+
assert.ok(!b.el('tw-frame-view').src, 'and nothing loaded');
|
|
695
|
+
await b.click('tw-open');
|
|
696
|
+
assert.deepEqual(b.opened.map((a) => a.slice(0, 2)), [['https://builders.cloudbongos.com/studio', '_blank']], 'the main button opens a new tab');
|
|
697
|
+
assert.equal(b.el('tw-openwrap').classList.contains('is-open'), false, 'and no menu');
|
|
698
|
+
await b.click('tw-open-more');
|
|
699
|
+
assert.ok(b.el('tw-openwrap').classList.contains('is-open'), 'the arrow opens the menu');
|
|
700
|
+
assert.equal(b.el('tw-open-more').getAttribute('aria-expanded'), 'true');
|
|
701
|
+
await b.click('tw-open-split');
|
|
702
|
+
assert.equal(b.el('tw-frame').hidden, false);
|
|
703
|
+
assert.equal(b.el('tw-frame-view').src, 'https://builders.cloudbongos.com/studio', 'the live page, in place');
|
|
704
|
+
assert.equal(b.el('tw-open-split').textContent, 'close split view');
|
|
705
|
+
assert.equal(b.el('tw-openwrap').classList.contains('is-open'), false, 'the menu closes after a choice');
|
|
706
|
+
assert.equal(b.el('tw-open').textContent, 'open in split view', 'the main button now repeats the split');
|
|
707
|
+
assert.equal(b.store['hall-tweak-open-as'], 'split');
|
|
708
|
+
assert.equal(b.store['hall-tweak-split'], 'on');
|
|
709
|
+
await b.click('tw-split-close');
|
|
710
|
+
assert.equal(b.el('tw-frame').hidden, true);
|
|
711
|
+
assert.equal(b.el('tw-frame-view').src, 'about:blank', 'closing unloads the page');
|
|
712
|
+
assert.equal(b.store['hall-tweak-split'], 'off');
|
|
713
|
+
await b.click('tw-open');
|
|
714
|
+
assert.equal(b.el('tw-frame').hidden, false, 'the main button reopens the split');
|
|
715
|
+
assert.equal(b.opened.length, 1, 'no second window for a hall page');
|
|
716
|
+
assert.equal(b.requests.filter((r) => r.method !== 'GET').length, 0, 'no write');
|
|
717
|
+
} finally { PAGE.url = saved; }
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
await test('the divider resizes by drag and by arrow keys, and the size is remembered', async () => {
|
|
721
|
+
const saved = PAGE.url;
|
|
722
|
+
PAGE.url = 'https://builders.{apex}/studio';
|
|
723
|
+
try {
|
|
724
|
+
const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) } });
|
|
725
|
+
await b.settle();
|
|
726
|
+
await b.click('tw-open-more');
|
|
727
|
+
await b.click('tw-open-split');
|
|
728
|
+
const div = b.el('tw-divider');
|
|
729
|
+
assert.equal(div.getAttribute('aria-valuenow'), '50');
|
|
730
|
+
b.fire('pointerdown', div, { clientY: 500, preventDefault() {} });
|
|
731
|
+
assert.ok(b.el('tw-frame').classList.contains('is-dragging'), 'the frame lets the pointer through while dragging');
|
|
732
|
+
b.fire('pointermove', div, { clientY: 700 });
|
|
733
|
+
assert.equal(div.getAttribute('aria-valuenow'), '30');
|
|
734
|
+
b.fire('pointerup', div, {});
|
|
735
|
+
assert.equal(b.el('tw-frame').classList.contains('is-dragging'), false);
|
|
736
|
+
assert.equal(b.store['hall-tweak-split-size'], '0.3');
|
|
737
|
+
b.fire('pointermove', div, { clientY: 100 });
|
|
738
|
+
assert.equal(div.getAttribute('aria-valuenow'), '30', 'moving after the drag ends changes nothing');
|
|
739
|
+
const ev = b.key('tw-divider', 'ArrowUp', false);
|
|
740
|
+
assert.equal(ev.prevented, true);
|
|
741
|
+
assert.equal(div.getAttribute('aria-valuenow'), '35');
|
|
742
|
+
assert.equal(b.store['hall-tweak-split-size'], '0.35');
|
|
743
|
+
} finally { PAGE.url = saved; }
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
await test('the next page opens the way the last one was left', async () => {
|
|
747
|
+
const saved = PAGE.url;
|
|
748
|
+
PAGE.url = 'https://builders.{apex}/studio';
|
|
749
|
+
try {
|
|
750
|
+
const b = bootEditor({
|
|
751
|
+
answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) },
|
|
752
|
+
stored: { 'hall-tweak-open-as': 'split', 'hall-tweak-split': 'on', 'hall-tweak-split-size': '0.4' },
|
|
753
|
+
});
|
|
754
|
+
await b.settle();
|
|
755
|
+
assert.equal(b.el('tw-frame').hidden, false, 'split view is back');
|
|
756
|
+
assert.equal(b.el('tw-divider').getAttribute('aria-valuenow'), '40', 'at its size');
|
|
757
|
+
assert.equal(b.el('tw-open').textContent, 'open in split view');
|
|
758
|
+
} finally { PAGE.url = saved; }
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
await test('a page on the apex splits into its own window docked to the bottom half, and never on load', async () => {
|
|
762
|
+
const b = bootEditor({
|
|
763
|
+
answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) },
|
|
764
|
+
stored: { 'hall-tweak-open-as': 'split', 'hall-tweak-split': 'on' },
|
|
765
|
+
});
|
|
766
|
+
await b.settle();
|
|
767
|
+
assert.equal(b.opened.length, 0, 'no popup on load: a browser would block it');
|
|
768
|
+
assert.equal(b.el('tw-frame').hidden, true, 'nothing framed: the site would refuse');
|
|
769
|
+
await b.click('tw-open');
|
|
770
|
+
assert.equal(b.opened.length, 1);
|
|
771
|
+
assert.deepEqual(b.opened[0].slice(0, 2), ['https://cloudbongos.com/', 'tw-live-page']);
|
|
772
|
+
assert.equal(b.opened[0][2], 'popup=yes,width=1600,height=500,left=0,top=500');
|
|
773
|
+
assert.equal(b.el('tw-frame').hidden, true);
|
|
774
|
+
});
|
|
775
|
+
|
|
471
776
|
await test('the page writes only the copy-desk routes it names', () => {
|
|
472
777
|
const js = read('tweak-editor.js');
|
|
473
778
|
const writes = [...js.matchAll(/api\.request\('(POST|PUT|PATCH|DELETE)', ([^,]+),/g)].map((m) => `${m[1]} ${m[2]}`);
|