@shardworks/astrolabe-apparatus 0.1.274 → 0.1.275
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-finalize.d.ts","sourceRoot":"","sources":["../../src/engines/plan-finalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAqC,MAAM,kCAAkC,CAAC;AACxG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAC;AAKzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"plan-finalize.d.ts","sourceRoot":"","sources":["../../src/engines/plan-finalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAqC,MAAM,kCAAkC,CAAC;AACxG,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAC;AAKzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAmB3C,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,YAAY,CA6ExF"}
|
|
@@ -38,14 +38,11 @@ const FRAMEWORK_EMITTER = 'framework';
|
|
|
38
38
|
* installed (astrolabe declares clockworks in `recommends`, not
|
|
39
39
|
* `requires`). Mirrors the lazy resolution `summon()` and the animator's
|
|
40
40
|
* `tryResolveClockworks` use for `LoomApi` and `ClockworksApi`.
|
|
41
|
+
* Delegates to the framework's `tryApparatus<T>` primitive — the
|
|
42
|
+
* optional-dependency counterpart to `apparatus<T>`.
|
|
41
43
|
*/
|
|
42
44
|
function tryResolveClockworks() {
|
|
43
|
-
|
|
44
|
-
return guild().apparatus('clockworks');
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
45
|
+
return guild().tryApparatus('clockworks');
|
|
49
46
|
}
|
|
50
47
|
export function createPlanFinalizeEngine(getPlansBook) {
|
|
51
48
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-finalize.js","sourceRoot":"","sources":["../../src/engines/plan-finalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAMH,OAAO,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAG/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,8BAA8B,EAAE,MAAM,iBAAiB,CAAC;AAEjE,8EAA8E;AAC9E,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAEtC
|
|
1
|
+
{"version":3,"file":"plan-finalize.js","sourceRoot":"","sources":["../../src/engines/plan-finalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAMH,OAAO,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAG/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,8BAA8B,EAAE,MAAM,iBAAiB,CAAC;AAEjE,8EAA8E;AAC9E,MAAM,iBAAiB,GAAG,WAAW,CAAC;AAEtC;;;;;;;GAOG;AACH,SAAS,oBAAoB;IAC3B,OAAO,KAAK,EAAE,CAAC,YAAY,CAAgB,YAAY,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,YAAiC;IACxE,OAAO;QACL,EAAE,EAAE,yBAAyB;QAE7B,KAAK,CAAC,GAAG,CACP,MAA+B,EAC/B,QAA0B;YAE1B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAgB,CAAC;YACvC,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;YAE5B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACpC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,SAAS,MAAM,cAAc,CAAC,CAAC;YACjD,CAAC;YAED,kBAAkB;YAClB,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC9B,MAAM,IAAI,KAAK,CACb,0DAA0D,IAAI,CAAC,MAAM,eAAe,MAAM,IAAI,CAC/F,CAAC;YACJ,CAAC;YAED,uBAAuB;YACvB,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC5D,MAAM,IAAI,KAAK,CACb,SAAS,MAAM,2DAA2D,CAC3E,CAAC;YACJ,CAAC;YAED,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YAEvB,kEAAkE;YAClE,8DAA8D;YAC9D,0CAA0C;YAC1C,MAAM,kBAAkB,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;YAEpD,yDAAyD;YACzD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACrC,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;gBACvB,MAAM,EAAE,WAAW;gBACnB,kBAAkB;gBAClB,SAAS,EAAE,GAAG;aACf,CAAC,CAAC;YAEH,iEAAiE;YACjE,kEAAkE;YAClE,gEAAgE;YAChE,6DAA6D;YAC7D,gCAAgC;YAChC,IAAI,kBAAkB,GAAG,CAAC,EAAE,CAAC;gBAC3B,MAAM,SAAS,GAAG,8BAA8B,EAAE,CAAC;gBACnD,IAAI,kBAAkB,GAAG,SAAS,EAAE,CAAC;oBACnC,MAAM,UAAU,GAAG,oBAAoB,EAAE,CAAC;oBAC1C,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;wBACxB,IAAI,CAAC;4BACH,MAAM,UAAU,CAAC,IAAI,CACnB,qCAAqC,EACrC,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,EAChD,iBAAiB,CAClB,CAAC;wBACJ,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;4BAChE,OAAO,CAAC,IAAI,CACV,iFAAiF,MAAM,EAAE,CAC1F,CAAC;wBACJ,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;YAED,OAAO;gBACL,MAAM,EAAE,WAAW;gBACnB,MAAM,EAAE,EAAE,IAAI,EAAE;aACjB,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardworks/astrolabe-apparatus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.275",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,18 +20,18 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"zod": "4.3.6",
|
|
23
|
-
"@shardworks/
|
|
24
|
-
"@shardworks/
|
|
25
|
-
"@shardworks/
|
|
26
|
-
"@shardworks/
|
|
27
|
-
"@shardworks/
|
|
28
|
-
"@shardworks/
|
|
29
|
-
"@shardworks/
|
|
30
|
-
"@shardworks/
|
|
23
|
+
"@shardworks/tools-apparatus": "0.1.275",
|
|
24
|
+
"@shardworks/stacks-apparatus": "0.1.275",
|
|
25
|
+
"@shardworks/clerk-apparatus": "0.1.275",
|
|
26
|
+
"@shardworks/spider-apparatus": "0.1.275",
|
|
27
|
+
"@shardworks/fabricator-apparatus": "0.1.275",
|
|
28
|
+
"@shardworks/clockworks-apparatus": "0.1.275",
|
|
29
|
+
"@shardworks/animator-apparatus": "0.1.275",
|
|
30
|
+
"@shardworks/loom-apparatus": "0.1.275"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "25.5.0",
|
|
34
|
-
"@shardworks/nexus-core": "0.1.
|
|
34
|
+
"@shardworks/nexus-core": "0.1.275"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
@@ -15,6 +15,37 @@
|
|
|
15
15
|
var LIMIT = 20;
|
|
16
16
|
var writTitleLookup = {};
|
|
17
17
|
|
|
18
|
+
// ── URL handling ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read the current querystring as a `URLSearchParams`. Live snapshot
|
|
22
|
+
* — read at call time, never cached.
|
|
23
|
+
*/
|
|
24
|
+
function currentUrlParams() {
|
|
25
|
+
return new URLSearchParams(window.location.search);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Apply the given key/value changes to the current querystring and
|
|
30
|
+
* `pushState` the result. Null/undefined/empty value deletes the key.
|
|
31
|
+
* Mirrors Ratchet's `updateUrl` (D9). The plan param shape (?plan=ID)
|
|
32
|
+
* is preserved verbatim — operators have URLs in the wild already
|
|
33
|
+
* (D10).
|
|
34
|
+
*/
|
|
35
|
+
function updateUrl(changes) {
|
|
36
|
+
var params = currentUrlParams();
|
|
37
|
+
var keys = Object.keys(changes);
|
|
38
|
+
for (var i = 0; i < keys.length; i++) {
|
|
39
|
+
var key = keys[i];
|
|
40
|
+
var value = changes[key];
|
|
41
|
+
if (value === null || value === undefined || value === '') params.delete(key);
|
|
42
|
+
else params.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
var qs = params.toString();
|
|
45
|
+
var next = window.location.pathname + (qs ? '?' + qs : '');
|
|
46
|
+
window.history.pushState({}, '', next);
|
|
47
|
+
}
|
|
48
|
+
|
|
18
49
|
// ── Utility ────────────────────────────────────────────────────────────
|
|
19
50
|
|
|
20
51
|
function esc(s) {
|
|
@@ -299,11 +330,41 @@
|
|
|
299
330
|
|
|
300
331
|
// ── Detail View ────────────────────────────────────────────────────────
|
|
301
332
|
|
|
302
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Render a "not found" empty state inside the plan detail view for a
|
|
335
|
+
* deep-linked id that does not resolve. Per D16 the URL param is
|
|
336
|
+
* preserved so the operator can recover (correct the id, hit Back).
|
|
337
|
+
* Replaces the legacy fall-back-to-list behaviour, which silently
|
|
338
|
+
* dropped the param and pretended the deep-link never happened.
|
|
339
|
+
*/
|
|
340
|
+
function renderPlanDetailNotFound(planId) {
|
|
341
|
+
currentPlan = null;
|
|
342
|
+
listView.style.display = 'none';
|
|
343
|
+
detailView.style.display = '';
|
|
344
|
+
detailTitle.textContent = 'Plan not found';
|
|
345
|
+
metaCard.innerHTML =
|
|
346
|
+
'<div class="empty-state" style="padding:1.5rem">' +
|
|
347
|
+
'No plan with id <code>' + esc(planId) + '</code> exists. ' +
|
|
348
|
+
'It may have been deleted, or the id may be mistyped.</div>';
|
|
349
|
+
// Hide tab content / decisions table — they reference fields on a
|
|
350
|
+
// missing plan that would just render blanks.
|
|
351
|
+
var tabContent = document.getElementById('tab-content');
|
|
352
|
+
if (tabContent) tabContent.innerHTML = '';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function showPlanDetail(plan, opts) {
|
|
356
|
+
var skipUrlPush = !!(opts && opts.skipUrlPush);
|
|
303
357
|
currentPlan = plan;
|
|
304
358
|
listView.style.display = 'none';
|
|
305
359
|
detailView.style.display = '';
|
|
306
360
|
|
|
361
|
+
// Centralised URL push — every entry path into showPlanDetail
|
|
362
|
+
// (row click, deep-link init, popstate-driven re-open) emits
|
|
363
|
+
// ?plan=ID for free. The popstate-driven path passes
|
|
364
|
+
// skipUrlPush=true to avoid double-pushing the URL the browser
|
|
365
|
+
// already updated.
|
|
366
|
+
if (!skipUrlPush) updateUrl({ plan: plan.id });
|
|
367
|
+
|
|
307
368
|
detailTitle.textContent = 'Plan: ' + plan.id;
|
|
308
369
|
|
|
309
370
|
// Metadata card
|
|
@@ -605,39 +666,67 @@
|
|
|
605
666
|
|
|
606
667
|
// ── Navigation ─────────────────────────────────────────────────────────
|
|
607
668
|
|
|
608
|
-
function backToList() {
|
|
669
|
+
function backToList(opts) {
|
|
670
|
+
var skipUrlPush = !!(opts && opts.skipUrlPush);
|
|
609
671
|
detailView.style.display = 'none';
|
|
610
672
|
listView.style.display = '';
|
|
611
673
|
currentPlan = null;
|
|
674
|
+
// D11: push a clean URL — the operator's Forward button still does
|
|
675
|
+
// what they expect, and we never pop history because they may have
|
|
676
|
+
// arrived directly at ?plan=ID with no prior list-view entry.
|
|
677
|
+
if (!skipUrlPush) updateUrl({ plan: null });
|
|
612
678
|
}
|
|
613
679
|
|
|
614
680
|
// ── Deep Link ──────────────────────────────────────────────────────────
|
|
615
681
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
682
|
+
/**
|
|
683
|
+
* Resolve `?plan=ID` to a detail view. Called on init and from the
|
|
684
|
+
* popstate handler. Both paths suppress the URL push (the browser
|
|
685
|
+
* already has the URL in place, or the deep-link landed already).
|
|
686
|
+
* A missing/deleted/mistyped id renders the not-found empty state
|
|
687
|
+
* (D16) — the URL param is left intact.
|
|
688
|
+
*
|
|
689
|
+
* `opts.fetchOnEmpty` selects what to do when no plan param is
|
|
690
|
+
* present. Init wants the list to be fetched (this is the page's
|
|
691
|
+
* normal load); popstate just wants to switch back to whatever was
|
|
692
|
+
* already rendered.
|
|
693
|
+
*/
|
|
694
|
+
function handleDeepLink(opts) {
|
|
695
|
+
var fetchOnEmpty = !(opts && opts.fetchOnEmpty === false);
|
|
696
|
+
var planId = currentUrlParams().get('plan');
|
|
619
697
|
|
|
620
698
|
if (planId) {
|
|
621
|
-
// Deep link: fetch and show the specific plan
|
|
622
699
|
fetch('/api/plan/show?planId=' + encodeURIComponent(planId))
|
|
623
700
|
.then(function (r) {
|
|
624
701
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
625
702
|
return r.json();
|
|
626
703
|
})
|
|
627
704
|
.then(function (plan) {
|
|
628
|
-
showPlanDetail(plan);
|
|
705
|
+
showPlanDetail(plan, { skipUrlPush: true });
|
|
629
706
|
})
|
|
630
707
|
.catch(function (err) {
|
|
631
708
|
console.error('Deep-link plan not found:', planId, err);
|
|
632
|
-
//
|
|
633
|
-
|
|
709
|
+
// D16: surface the failure as a not-found empty state inside
|
|
710
|
+
// the detail panel; never silently rewrite the URL.
|
|
711
|
+
renderPlanDetailNotFound(planId);
|
|
634
712
|
});
|
|
635
|
-
} else {
|
|
713
|
+
} else if (fetchOnEmpty) {
|
|
636
714
|
fetchPlans(true);
|
|
715
|
+
} else {
|
|
716
|
+
// popstate to a no-?plan URL: just return to the list view
|
|
717
|
+
// without re-fetching.
|
|
718
|
+
backToList({ skipUrlPush: true });
|
|
637
719
|
}
|
|
638
720
|
}
|
|
639
721
|
|
|
640
722
|
// ── Init ───────────────────────────────────────────────────────────────
|
|
641
723
|
|
|
724
|
+
// popstate listener — the browser updated the URL, so we re-run the
|
|
725
|
+
// deep-link routing without re-pushing. Pairs with the central push
|
|
726
|
+
// inside showPlanDetail (D11/D12).
|
|
727
|
+
window.addEventListener('popstate', function () {
|
|
728
|
+
handleDeepLink({ fetchOnEmpty: false });
|
|
729
|
+
});
|
|
730
|
+
|
|
642
731
|
handleDeepLink();
|
|
643
732
|
})();
|
|
@@ -708,3 +708,101 @@ describe('astrolabe.js cost-panel rig lookup uses rig-for-writ', () => {
|
|
|
708
708
|
);
|
|
709
709
|
});
|
|
710
710
|
});
|
|
711
|
+
|
|
712
|
+
// ── Deep-link URL state (?plan=ID) ──────────────────────────────────────
|
|
713
|
+
|
|
714
|
+
describe('astrolabe.js — deep-link URL state', () => {
|
|
715
|
+
it('exposes currentUrlParams + updateUrl helpers (D9 — Ratchet pattern)', () => {
|
|
716
|
+
assert.match(
|
|
717
|
+
astrolabeJs,
|
|
718
|
+
/function currentUrlParams\(\)\s*\{[\s\S]*?new URLSearchParams\(window\.location\.search\)/,
|
|
719
|
+
'currentUrlParams reads window.location.search live',
|
|
720
|
+
);
|
|
721
|
+
assert.match(
|
|
722
|
+
astrolabeJs,
|
|
723
|
+
/function updateUrl\(changes\)\s*\{[\s\S]*?window\.history\.pushState/,
|
|
724
|
+
'updateUrl pushes via history.pushState',
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('showPlanDetail pushes ?plan=ID via the central updateUrl call (D12)', () => {
|
|
729
|
+
const block = astrolabeJs.match(
|
|
730
|
+
/function showPlanDetail\(plan(?:, opts)?\)[\s\S]*?(?=\n {2}function )/,
|
|
731
|
+
);
|
|
732
|
+
assert.ok(block, 'should find showPlanDetail body');
|
|
733
|
+
assert.match(
|
|
734
|
+
block[0],
|
|
735
|
+
/updateUrl\(\{\s*plan:\s*plan\.id\s*\}\)/,
|
|
736
|
+
'showPlanDetail should push ?plan=<plan.id> when not skipUrlPush',
|
|
737
|
+
);
|
|
738
|
+
assert.match(block[0], /skipUrlPush/, 'showPlanDetail accepts a skipUrlPush opt');
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('backToList clears ?plan via updateUrl({plan: null}) — never pops history (D11)', () => {
|
|
742
|
+
const block = astrolabeJs.match(
|
|
743
|
+
/function backToList\((?:opts)?\)[\s\S]*?(?=\n {2}\/\/)/,
|
|
744
|
+
);
|
|
745
|
+
assert.ok(block, 'should find backToList body');
|
|
746
|
+
assert.match(
|
|
747
|
+
block[0],
|
|
748
|
+
/updateUrl\(\{\s*plan:\s*null\s*\}\)/,
|
|
749
|
+
'backToList should push a clean URL via updateUrl({plan: null})',
|
|
750
|
+
);
|
|
751
|
+
assert.ok(
|
|
752
|
+
!/window\.history\.back\s*\(/.test(block[0]),
|
|
753
|
+
'backToList should never invoke history.back',
|
|
754
|
+
);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('a popstate listener re-runs handleDeepLink without re-pushing the URL', () => {
|
|
758
|
+
assert.match(
|
|
759
|
+
astrolabeJs,
|
|
760
|
+
/window\.addEventListener\(\s*['"]popstate['"]/,
|
|
761
|
+
'astrolabe.js registers a popstate listener',
|
|
762
|
+
);
|
|
763
|
+
const block = astrolabeJs.match(
|
|
764
|
+
/addEventListener\(\s*['"]popstate['"][\s\S]*?\}\)/,
|
|
765
|
+
);
|
|
766
|
+
assert.ok(block, 'should find popstate handler body');
|
|
767
|
+
assert.match(
|
|
768
|
+
block[0],
|
|
769
|
+
/handleDeepLink\(\{\s*fetchOnEmpty:\s*false\s*\}\)/,
|
|
770
|
+
'popstate handler invokes handleDeepLink with fetchOnEmpty=false',
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('handleDeepLink renders a not-found state instead of falling back to the list (D16)', () => {
|
|
775
|
+
const block = astrolabeJs.match(
|
|
776
|
+
/function handleDeepLink\([\s\S]*?(?=\n {2}\/\/ ── Init)/,
|
|
777
|
+
);
|
|
778
|
+
assert.ok(block, 'should find handleDeepLink body');
|
|
779
|
+
assert.match(
|
|
780
|
+
block[0],
|
|
781
|
+
/renderPlanDetailNotFound\(planId\)/,
|
|
782
|
+
'handleDeepLink should render the not-found state on fetch failure',
|
|
783
|
+
);
|
|
784
|
+
// The deep-link calls into showPlanDetail with skipUrlPush so we
|
|
785
|
+
// don't double-push the URL the operator already arrived with.
|
|
786
|
+
assert.match(
|
|
787
|
+
block[0],
|
|
788
|
+
/showPlanDetail\(plan,\s*\{\s*skipUrlPush:\s*true\s*\}\)/,
|
|
789
|
+
'init / popstate path passes skipUrlPush=true into showPlanDetail',
|
|
790
|
+
);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('renderPlanDetailNotFound preserves the URL param', () => {
|
|
794
|
+
const block = astrolabeJs.match(
|
|
795
|
+
/function renderPlanDetailNotFound\([\s\S]*?(?=\n {2}function )/,
|
|
796
|
+
);
|
|
797
|
+
assert.ok(block, 'should find renderPlanDetailNotFound body');
|
|
798
|
+
assert.ok(
|
|
799
|
+
!/updateUrl/.test(block[0]),
|
|
800
|
+
'renderPlanDetailNotFound must not rewrite the URL',
|
|
801
|
+
);
|
|
802
|
+
assert.match(
|
|
803
|
+
block[0],
|
|
804
|
+
/No plan with id/,
|
|
805
|
+
'renderPlanDetailNotFound surfaces a "not found" message',
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
});
|