@ascendenceai/cortena-extensions-e2e-kit 0.1.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.
Files changed (42) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +126 -0
  3. package/dist/config.d.ts +59 -0
  4. package/dist/config.js +63 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/env.d.ts +43 -0
  7. package/dist/env.js +67 -0
  8. package/dist/env.js.map +1 -0
  9. package/dist/fixtures/consent.d.ts +107 -0
  10. package/dist/fixtures/consent.js +220 -0
  11. package/dist/fixtures/consent.js.map +1 -0
  12. package/dist/fixtures/embedded.d.ts +47 -0
  13. package/dist/fixtures/embedded.js +60 -0
  14. package/dist/fixtures/embedded.js.map +1 -0
  15. package/dist/fixtures/list-state.d.ts +43 -0
  16. package/dist/fixtures/list-state.js +58 -0
  17. package/dist/fixtures/list-state.js.map +1 -0
  18. package/dist/fixtures/popup.d.ts +44 -0
  19. package/dist/fixtures/popup.js +81 -0
  20. package/dist/fixtures/popup.js.map +1 -0
  21. package/dist/fixtures/shell.d.ts +63 -0
  22. package/dist/fixtures/shell.js +96 -0
  23. package/dist/fixtures/shell.js.map +1 -0
  24. package/dist/fixtures/sign-in.d.ts +59 -0
  25. package/dist/fixtures/sign-in.js +115 -0
  26. package/dist/fixtures/sign-in.js.map +1 -0
  27. package/dist/fixtures/theme.d.ts +42 -0
  28. package/dist/fixtures/theme.js +79 -0
  29. package/dist/fixtures/theme.js.map +1 -0
  30. package/dist/index.d.ts +34 -0
  31. package/dist/index.js +25 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/locators.d.ts +210 -0
  34. package/dist/locators.js +212 -0
  35. package/dist/locators.js.map +1 -0
  36. package/dist/protocol/suite.d.ts +31 -0
  37. package/dist/protocol/suite.js +196 -0
  38. package/dist/protocol/suite.js.map +1 -0
  39. package/dist/protocol/types.d.ts +61 -0
  40. package/dist/protocol/types.js +4 -0
  41. package/dist/protocol/types.js.map +1 -0
  42. package/package.json +58 -0
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Every selector the kit uses, in one file.
3
+ *
4
+ * This is the seam the whole package exists for. Six extensions writing their
5
+ * own layer-3 suites means six copies of "how do I find the sidebar", and the
6
+ * copies drift on the one layer that is supposed to prove the fleet looks the
7
+ * same. Here the shell is found once; a renamed `data-slot` in cortena-ui
8
+ * breaks every adopter's suite in this file rather than in six of them.
9
+ *
10
+ * ## Why `data-slot` and not a role or a test id
11
+ *
12
+ * `data-slot` is cortena-ui's own attribute, written by the component for its
13
+ * own styling. It is therefore load-bearing: it cannot be deleted as "test
14
+ * scaffolding" by somebody who does not run the suite, which is exactly what
15
+ * happens to a `data-testid` that only a Playwright file reads. Roles and
16
+ * accessible names are used where the component's contract IS the role — the
17
+ * avatar menu's items, the agent composer — because those are what a person
18
+ * uses and what a screen reader announces.
19
+ *
20
+ * ## DESIGN-119
21
+ *
22
+ * DESIGN-119 changes how sign-in and the signed-out state are mounted: today
23
+ * an extension may draw its own login form (Tasks does, with an organisation
24
+ * step cortena-ui's `LoginScreen` has no equivalent of), and afterwards the
25
+ * frame composition mounts the shared screen. Everything under `signIn` below
26
+ * is therefore written twice — the shared `data-slot`s first, the extension's
27
+ * own form as the fallback — and `SIGN_IN` is the only export that moves when
28
+ * DESIGN-119 lands. Nothing outside this file names a sign-in element.
29
+ */
30
+ /** The cortena-ui version these selectors were read out of. */
31
+ export declare const CORTENA_UI = "1.13.0";
32
+ /**
33
+ * `AppShell` — cortena-ui/src/components/app-shell.tsx.
34
+ *
35
+ * `NAV_DRAWER_ID` is exported by the package as `cortena-nav-drawer` and
36
+ * `HELP_PANEL_ID` as `cortena-help-panel`; both are written here as the ids
37
+ * they are, because a Playwright file cannot import a React module.
38
+ */
39
+ export declare const SHELL: {
40
+ readonly root: string;
41
+ readonly header: string;
42
+ readonly brand: string;
43
+ readonly brandSquare: string;
44
+ /**
45
+ * The registered mark. `data-mark` carries the mark's id, or the literal
46
+ * `fallback` when nothing is registered for the extension — which renders an
47
+ * initial rather than an empty box, and is worth failing on.
48
+ */
49
+ readonly brandMark: string;
50
+ readonly markAttribute: "data-mark";
51
+ readonly brandName: string;
52
+ readonly navToggle: string;
53
+ readonly nav: string;
54
+ readonly navList: string;
55
+ readonly navItem: string;
56
+ /** One row by the id the extension gave it: `[data-nav-item="my-tasks"]`. */
57
+ readonly navItemById: (id: string) => string;
58
+ /** The row a screen reader hears as the current page, not the one that merely looks lit. */
59
+ readonly navItemCurrent: "[data-slot=\"app-shell-nav-item\"][aria-current=\"page\"]";
60
+ readonly navGroupLabel: string;
61
+ readonly navDrawer: "#cortena-nav-drawer";
62
+ readonly navDrawerClose: string;
63
+ readonly navScrim: string;
64
+ readonly main: string;
65
+ readonly avatarTrigger: string;
66
+ /** The avatar menu's rows carry `data-item`: profile, settings, logout, and the extension's own. */
67
+ readonly menuItem: (id: string) => string;
68
+ /** `BottomRightCluster` — theme, help, and the agent pill stacked above them. */
69
+ readonly cluster: string;
70
+ readonly clusterRow: string;
71
+ readonly clusterAgent: string;
72
+ readonly themeToggle: string;
73
+ readonly helpButton: string;
74
+ /**
75
+ * The shell's own help region — `<aside id="cortena-help-panel">`. Not
76
+ * `[data-slot="help-panel"]`: that is cortena-ui's markdown `HelpPanel`, and
77
+ * an extension whose functional document is HTML renders a frame inside this
78
+ * aside instead (Tasks does). The aside is the part every adopter has.
79
+ */
80
+ readonly helpPanel: string;
81
+ readonly helpPanelById: "#cortena-help-panel";
82
+ };
83
+ /**
84
+ * `AgentChatPopup` and `AgentChat` — cortena-ui/src/components/agent-chat-popup.tsx
85
+ * and agent-chat.tsx, reached through the `cortena-ui/agent-chat` entry.
86
+ */
87
+ export declare const AGENT: {
88
+ readonly pill: string;
89
+ readonly pillUnread: string;
90
+ readonly panel: string;
91
+ readonly collapse: string;
92
+ readonly expand: string;
93
+ readonly chat: string;
94
+ readonly scroll: string;
95
+ readonly input: string;
96
+ readonly send: string;
97
+ readonly stop: string;
98
+ readonly turn: string;
99
+ readonly message: string;
100
+ readonly userMessage: "[data-slot=\"agent-chat-message\"][data-role=\"user\"]";
101
+ readonly assistantMessage: "[data-slot=\"agent-chat-message\"][data-role=\"assistant\"]";
102
+ readonly errorMessage: "[data-slot=\"agent-chat-message\"][data-role=\"error\"]";
103
+ readonly streaming: string;
104
+ readonly streamingText: string;
105
+ /** The A2UI block the protocol requires the pop-up to be able to draw (§19, P-25). */
106
+ readonly a2ui: string;
107
+ readonly toolCard: string;
108
+ readonly newSession: string;
109
+ readonly sessionsTrigger: string;
110
+ readonly session: string;
111
+ readonly empty: string;
112
+ };
113
+ /**
114
+ * `SessionGuard`'s expiry dialog — §10.4, rule P-37. The kit does not drive
115
+ * session expiry (it needs the idle policy shortened, which is an extension's
116
+ * own switch), but `signIn` has to be able to tell "signed out" from "broken".
117
+ */
118
+ export declare const SESSION: {
119
+ readonly dialog: string;
120
+ readonly countdown: string;
121
+ readonly continue: string;
122
+ readonly logout: string;
123
+ };
124
+ /**
125
+ * Sign-in. **This is the block DESIGN-119 moves.**
126
+ *
127
+ * `shared` is cortena-ui's `AuthScreen`/`LoginScreen` composition, which is
128
+ * what an extension will mount once DESIGN-119 lands. `own` is the form an
129
+ * extension draws itself today — labelled controls and a submit button, which
130
+ * is all that can be assumed of a form nobody shared. `signIn()` prefers the
131
+ * shared slots and falls back to the labels, so an extension that has already
132
+ * adopted the shared screen and one that has not both sign in through the same
133
+ * call, and the fallback deletes itself when the last extension migrates.
134
+ */
135
+ export declare const SIGN_IN: {
136
+ readonly shared: {
137
+ readonly screen: string;
138
+ readonly card: string;
139
+ readonly title: string;
140
+ readonly brandName: string;
141
+ readonly form: string;
142
+ readonly submit: string;
143
+ readonly error: string;
144
+ /** The "You were signed out" notice §10.4.1 asks for. */
145
+ readonly signedOut: string;
146
+ readonly returnTo: string;
147
+ readonly providers: string;
148
+ };
149
+ /**
150
+ * The extension's own form, by accessible name. Deliberately not a
151
+ * `data-slot`: an extension's own markup has none to promise.
152
+ */
153
+ readonly own: {
154
+ readonly orgLabel: RegExp;
155
+ readonly orgSubmit: RegExp;
156
+ readonly emailLabel: RegExp;
157
+ readonly passwordLabel: RegExp;
158
+ readonly submit: RegExp;
159
+ };
160
+ };
161
+ /**
162
+ * The MCP consent page — the one screen in the six that no extension renders.
163
+ * It is cortena-auth's hosted page, so there is no cortena-ui slot to key on
164
+ * and no version of this repository can promise one. It is located by what a
165
+ * consent page *is*: a form naming the client, listing the scopes, with an
166
+ * allow and a deny. `ConsentScreen`'s slots are tried first for the day
167
+ * cortena-auth adopts it.
168
+ */
169
+ export declare const CONSENT: {
170
+ readonly screen: string;
171
+ readonly scopes: string;
172
+ readonly scope: string;
173
+ readonly actions: string;
174
+ /** The form-shaped fallback, which is what is actually there today. */
175
+ readonly byForm: {
176
+ readonly allow: RegExp;
177
+ readonly deny: RegExp;
178
+ /** cortena-auth's own list of what is being asked for. */
179
+ readonly scopeList: ".scope-list";
180
+ /** The organisation picker, when the user is a member of more than one. */
181
+ readonly orgPick: ".org-pick";
182
+ };
183
+ /**
184
+ * cortena-auth's own sign-in, which the authorisation request lands on
185
+ * before it will show a consent page — and which asks for the organisation
186
+ * first, exactly as an extension's own login does.
187
+ */
188
+ readonly signIn: {
189
+ readonly org: RegExp;
190
+ readonly orgSubmit: RegExp;
191
+ readonly email: RegExp;
192
+ readonly password: RegExp;
193
+ readonly submit: RegExp;
194
+ };
195
+ };
196
+ /**
197
+ * The theme contract, from cortena-ui's `useCortenaTheme`.
198
+ *
199
+ * cortena-design drives dark mode off `data-theme` on `<html>` and falls back
200
+ * to `prefers-color-scheme` when the attribute is absent — so "the OS path" is
201
+ * literally the absence of the attribute, and a suite that only ever writes it
202
+ * never tests the fallback the majority of first visits take.
203
+ */
204
+ export declare const THEME: {
205
+ readonly attribute: "data-theme";
206
+ readonly storageKey: "cortena.theme";
207
+ /** The toggle publishes both the stored choice and what is painted. */
208
+ readonly choiceAttribute: "data-theme-choice";
209
+ readonly resolvedAttribute: "data-theme-resolved";
210
+ };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Every selector the kit uses, in one file.
3
+ *
4
+ * This is the seam the whole package exists for. Six extensions writing their
5
+ * own layer-3 suites means six copies of "how do I find the sidebar", and the
6
+ * copies drift on the one layer that is supposed to prove the fleet looks the
7
+ * same. Here the shell is found once; a renamed `data-slot` in cortena-ui
8
+ * breaks every adopter's suite in this file rather than in six of them.
9
+ *
10
+ * ## Why `data-slot` and not a role or a test id
11
+ *
12
+ * `data-slot` is cortena-ui's own attribute, written by the component for its
13
+ * own styling. It is therefore load-bearing: it cannot be deleted as "test
14
+ * scaffolding" by somebody who does not run the suite, which is exactly what
15
+ * happens to a `data-testid` that only a Playwright file reads. Roles and
16
+ * accessible names are used where the component's contract IS the role — the
17
+ * avatar menu's items, the agent composer — because those are what a person
18
+ * uses and what a screen reader announces.
19
+ *
20
+ * ## DESIGN-119
21
+ *
22
+ * DESIGN-119 changes how sign-in and the signed-out state are mounted: today
23
+ * an extension may draw its own login form (Tasks does, with an organisation
24
+ * step cortena-ui's `LoginScreen` has no equivalent of), and afterwards the
25
+ * frame composition mounts the shared screen. Everything under `signIn` below
26
+ * is therefore written twice — the shared `data-slot`s first, the extension's
27
+ * own form as the fallback — and `SIGN_IN` is the only export that moves when
28
+ * DESIGN-119 lands. Nothing outside this file names a sign-in element.
29
+ */
30
+ /** The cortena-ui version these selectors were read out of. */
31
+ export const CORTENA_UI = '1.13.0';
32
+ const slot = (name) => `[data-slot="${name}"]`;
33
+ /**
34
+ * `AppShell` — cortena-ui/src/components/app-shell.tsx.
35
+ *
36
+ * `NAV_DRAWER_ID` is exported by the package as `cortena-nav-drawer` and
37
+ * `HELP_PANEL_ID` as `cortena-help-panel`; both are written here as the ids
38
+ * they are, because a Playwright file cannot import a React module.
39
+ */
40
+ export const SHELL = {
41
+ root: slot('app-shell'),
42
+ header: slot('app-shell-header'),
43
+ brand: slot('app-shell-brand'),
44
+ brandSquare: slot('app-shell-brand-square'),
45
+ /**
46
+ * The registered mark. `data-mark` carries the mark's id, or the literal
47
+ * `fallback` when nothing is registered for the extension — which renders an
48
+ * initial rather than an empty box, and is worth failing on.
49
+ */
50
+ brandMark: slot('brand-mark'),
51
+ markAttribute: 'data-mark',
52
+ brandName: slot('app-shell-brand-name'),
53
+ navToggle: slot('app-shell-nav-toggle'),
54
+ nav: slot('app-shell-nav'),
55
+ navList: slot('app-shell-nav-list'),
56
+ navItem: slot('app-shell-nav-item'),
57
+ /** One row by the id the extension gave it: `[data-nav-item="my-tasks"]`. */
58
+ navItemById: (id) => `[data-nav-item="${id}"]`,
59
+ /** The row a screen reader hears as the current page, not the one that merely looks lit. */
60
+ navItemCurrent: '[data-slot="app-shell-nav-item"][aria-current="page"]',
61
+ navGroupLabel: slot('app-shell-nav-group-label'),
62
+ navDrawer: '#cortena-nav-drawer',
63
+ navDrawerClose: slot('app-shell-nav-drawer-close'),
64
+ navScrim: slot('app-shell-nav-scrim'),
65
+ main: slot('app-shell-main'),
66
+ avatarTrigger: slot('app-shell-avatar-trigger'),
67
+ /** The avatar menu's rows carry `data-item`: profile, settings, logout, and the extension's own. */
68
+ menuItem: (id) => `[data-item="${id}"]`,
69
+ /** `BottomRightCluster` — theme, help, and the agent pill stacked above them. */
70
+ cluster: slot('bottom-right-cluster'),
71
+ clusterRow: slot('bottom-right-cluster-row'),
72
+ clusterAgent: slot('bottom-right-cluster-agent'),
73
+ themeToggle: slot('theme-toggle'),
74
+ helpButton: slot('help-button'),
75
+ /**
76
+ * The shell's own help region — `<aside id="cortena-help-panel">`. Not
77
+ * `[data-slot="help-panel"]`: that is cortena-ui's markdown `HelpPanel`, and
78
+ * an extension whose functional document is HTML renders a frame inside this
79
+ * aside instead (Tasks does). The aside is the part every adopter has.
80
+ */
81
+ helpPanel: slot('app-shell-help-panel'),
82
+ helpPanelById: '#cortena-help-panel',
83
+ };
84
+ /**
85
+ * `AgentChatPopup` and `AgentChat` — cortena-ui/src/components/agent-chat-popup.tsx
86
+ * and agent-chat.tsx, reached through the `cortena-ui/agent-chat` entry.
87
+ */
88
+ export const AGENT = {
89
+ pill: slot('agent-chat-pill'),
90
+ pillUnread: slot('agent-chat-pill-unread'),
91
+ panel: slot('agent-chat-panel'),
92
+ collapse: slot('agent-chat-collapse'),
93
+ expand: slot('agent-chat-expand'),
94
+ chat: slot('agent-chat'),
95
+ scroll: slot('agent-chat-scroll'),
96
+ input: slot('agent-chat-input'),
97
+ send: slot('agent-chat-send'),
98
+ stop: slot('agent-chat-stop'),
99
+ turn: slot('agent-chat-turn'),
100
+ message: slot('agent-chat-message'),
101
+ userMessage: '[data-slot="agent-chat-message"][data-role="user"]',
102
+ assistantMessage: '[data-slot="agent-chat-message"][data-role="assistant"]',
103
+ errorMessage: '[data-slot="agent-chat-message"][data-role="error"]',
104
+ streaming: slot('agent-chat-streaming'),
105
+ streamingText: slot('agent-chat-streaming-text'),
106
+ /** The A2UI block the protocol requires the pop-up to be able to draw (§19, P-25). */
107
+ a2ui: slot('agent-chat-a2ui'),
108
+ toolCard: slot('agent-tool-card'),
109
+ newSession: slot('agent-chat-new-session'),
110
+ sessionsTrigger: slot('agent-chat-sessions-trigger'),
111
+ session: slot('agent-chat-session'),
112
+ empty: slot('agent-chat-empty'),
113
+ };
114
+ /**
115
+ * `SessionGuard`'s expiry dialog — §10.4, rule P-37. The kit does not drive
116
+ * session expiry (it needs the idle policy shortened, which is an extension's
117
+ * own switch), but `signIn` has to be able to tell "signed out" from "broken".
118
+ */
119
+ export const SESSION = {
120
+ dialog: slot('session-expiry-dialog'),
121
+ countdown: slot('session-expiry-countdown'),
122
+ continue: slot('session-expiry-continue'),
123
+ logout: slot('session-expiry-logout'),
124
+ };
125
+ /**
126
+ * Sign-in. **This is the block DESIGN-119 moves.**
127
+ *
128
+ * `shared` is cortena-ui's `AuthScreen`/`LoginScreen` composition, which is
129
+ * what an extension will mount once DESIGN-119 lands. `own` is the form an
130
+ * extension draws itself today — labelled controls and a submit button, which
131
+ * is all that can be assumed of a form nobody shared. `signIn()` prefers the
132
+ * shared slots and falls back to the labels, so an extension that has already
133
+ * adopted the shared screen and one that has not both sign in through the same
134
+ * call, and the fallback deletes itself when the last extension migrates.
135
+ */
136
+ export const SIGN_IN = {
137
+ shared: {
138
+ screen: slot('auth-screen'),
139
+ card: slot('auth-screen-card'),
140
+ title: slot('auth-screen-title'),
141
+ brandName: slot('auth-screen-brand-name'),
142
+ form: slot('login-screen-form'),
143
+ submit: slot('login-screen-submit'),
144
+ error: slot('login-screen-error'),
145
+ /** The "You were signed out" notice §10.4.1 asks for. */
146
+ signedOut: slot('login-screen-signed-out'),
147
+ returnTo: slot('login-screen-return-to'),
148
+ providers: slot('login-screen-providers'),
149
+ },
150
+ /**
151
+ * The extension's own form, by accessible name. Deliberately not a
152
+ * `data-slot`: an extension's own markup has none to promise.
153
+ */
154
+ own: {
155
+ orgLabel: /organi[sz]ation/i,
156
+ orgSubmit: /^continue$/i,
157
+ emailLabel: /^e-?mail$/i,
158
+ passwordLabel: /^password$/i,
159
+ submit: /^sign in$/i,
160
+ },
161
+ };
162
+ /**
163
+ * The MCP consent page — the one screen in the six that no extension renders.
164
+ * It is cortena-auth's hosted page, so there is no cortena-ui slot to key on
165
+ * and no version of this repository can promise one. It is located by what a
166
+ * consent page *is*: a form naming the client, listing the scopes, with an
167
+ * allow and a deny. `ConsentScreen`'s slots are tried first for the day
168
+ * cortena-auth adopts it.
169
+ */
170
+ export const CONSENT = {
171
+ screen: slot('consent-screen'),
172
+ scopes: slot('consent-screen-scopes'),
173
+ scope: slot('consent-scope'),
174
+ actions: slot('consent-screen-actions'),
175
+ /** The form-shaped fallback, which is what is actually there today. */
176
+ byForm: {
177
+ allow: /^(allow|authori[sz]e|approve|continue)$/i,
178
+ deny: /^(deny|cancel|reject)$/i,
179
+ /** cortena-auth's own list of what is being asked for. */
180
+ scopeList: '.scope-list',
181
+ /** The organisation picker, when the user is a member of more than one. */
182
+ orgPick: '.org-pick',
183
+ },
184
+ /**
185
+ * cortena-auth's own sign-in, which the authorisation request lands on
186
+ * before it will show a consent page — and which asks for the organisation
187
+ * first, exactly as an extension's own login does.
188
+ */
189
+ signIn: {
190
+ org: /organi[sz]ation/i,
191
+ orgSubmit: /^continue$/i,
192
+ email: /^e-?mail$/i,
193
+ password: /^password$/i,
194
+ submit: /^sign in$/i,
195
+ },
196
+ };
197
+ /**
198
+ * The theme contract, from cortena-ui's `useCortenaTheme`.
199
+ *
200
+ * cortena-design drives dark mode off `data-theme` on `<html>` and falls back
201
+ * to `prefers-color-scheme` when the attribute is absent — so "the OS path" is
202
+ * literally the absence of the attribute, and a suite that only ever writes it
203
+ * never tests the fallback the majority of first visits take.
204
+ */
205
+ export const THEME = {
206
+ attribute: 'data-theme',
207
+ storageKey: 'cortena.theme',
208
+ /** The toggle publishes both the stored choice and what is painted. */
209
+ choiceAttribute: 'data-theme-choice',
210
+ resolvedAttribute: 'data-theme-resolved',
211
+ };
212
+ //# sourceMappingURL=locators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locators.js","sourceRoot":"","sources":["../src/locators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAAC;AAEnC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,eAAe,IAAI,IAAI,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC;IACvB,MAAM,EAAE,IAAI,CAAC,kBAAkB,CAAC;IAChC,KAAK,EAAE,IAAI,CAAC,iBAAiB,CAAC;IAC9B,WAAW,EAAE,IAAI,CAAC,wBAAwB,CAAC;IAC3C;;;;OAIG;IACH,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC;IAC7B,aAAa,EAAE,WAAW;IAC1B,SAAS,EAAE,IAAI,CAAC,sBAAsB,CAAC;IACvC,SAAS,EAAE,IAAI,CAAC,sBAAsB,CAAC;IACvC,GAAG,EAAE,IAAI,CAAC,eAAe,CAAC;IAC1B,OAAO,EAAE,IAAI,CAAC,oBAAoB,CAAC;IACnC,OAAO,EAAE,IAAI,CAAC,oBAAoB,CAAC;IACnC,6EAA6E;IAC7E,WAAW,EAAE,CAAC,EAAU,EAAU,EAAE,CAAC,mBAAmB,EAAE,IAAI;IAC9D,4FAA4F;IAC5F,cAAc,EAAE,uDAAuD;IACvE,aAAa,EAAE,IAAI,CAAC,2BAA2B,CAAC;IAChD,SAAS,EAAE,qBAAqB;IAChC,cAAc,EAAE,IAAI,CAAC,4BAA4B,CAAC;IAClD,QAAQ,EAAE,IAAI,CAAC,qBAAqB,CAAC;IACrC,IAAI,EAAE,IAAI,CAAC,gBAAgB,CAAC;IAC5B,aAAa,EAAE,IAAI,CAAC,0BAA0B,CAAC;IAC/C,oGAAoG;IACpG,QAAQ,EAAE,CAAC,EAAU,EAAU,EAAE,CAAC,eAAe,EAAE,IAAI;IACvD,iFAAiF;IACjF,OAAO,EAAE,IAAI,CAAC,sBAAsB,CAAC;IACrC,UAAU,EAAE,IAAI,CAAC,0BAA0B,CAAC;IAC5C,YAAY,EAAE,IAAI,CAAC,4BAA4B,CAAC;IAChD,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC;IACjC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B;;;;;OAKG;IACH,SAAS,EAAE,IAAI,CAAC,sBAAsB,CAAC;IACvC,aAAa,EAAE,qBAAqB;CAC5B,CAAC;AAEX;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,IAAI,EAAE,IAAI,CAAC,iBAAiB,CAAC;IAC7B,UAAU,EAAE,IAAI,CAAC,wBAAwB,CAAC;IAC1C,KAAK,EAAE,IAAI,CAAC,kBAAkB,CAAC;IAC/B,QAAQ,EAAE,IAAI,CAAC,qBAAqB,CAAC;IACrC,MAAM,EAAE,IAAI,CAAC,mBAAmB,CAAC;IACjC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC;IACxB,MAAM,EAAE,IAAI,CAAC,mBAAmB,CAAC;IACjC,KAAK,EAAE,IAAI,CAAC,kBAAkB,CAAC;IAC/B,IAAI,EAAE,IAAI,CAAC,iBAAiB,CAAC;IAC7B,IAAI,EAAE,IAAI,CAAC,iBAAiB,CAAC;IAC7B,IAAI,EAAE,IAAI,CAAC,iBAAiB,CAAC;IAC7B,OAAO,EAAE,IAAI,CAAC,oBAAoB,CAAC;IACnC,WAAW,EAAE,oDAAoD;IACjE,gBAAgB,EAAE,yDAAyD;IAC3E,YAAY,EAAE,qDAAqD;IACnE,SAAS,EAAE,IAAI,CAAC,sBAAsB,CAAC;IACvC,aAAa,EAAE,IAAI,CAAC,2BAA2B,CAAC;IAChD,sFAAsF;IACtF,IAAI,EAAE,IAAI,CAAC,iBAAiB,CAAC;IAC7B,QAAQ,EAAE,IAAI,CAAC,iBAAiB,CAAC;IACjC,UAAU,EAAE,IAAI,CAAC,wBAAwB,CAAC;IAC1C,eAAe,EAAE,IAAI,CAAC,6BAA6B,CAAC;IACpD,OAAO,EAAE,IAAI,CAAC,oBAAoB,CAAC;IACnC,KAAK,EAAE,IAAI,CAAC,kBAAkB,CAAC;CACvB,CAAC;AAEX;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,MAAM,EAAE,IAAI,CAAC,uBAAuB,CAAC;IACrC,SAAS,EAAE,IAAI,CAAC,0BAA0B,CAAC;IAC3C,QAAQ,EAAE,IAAI,CAAC,yBAAyB,CAAC;IACzC,MAAM,EAAE,IAAI,CAAC,uBAAuB,CAAC;CAC7B,CAAC;AAEX;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,MAAM,EAAE;QACN,MAAM,EAAE,IAAI,CAAC,aAAa,CAAC;QAC3B,IAAI,EAAE,IAAI,CAAC,kBAAkB,CAAC;QAC9B,KAAK,EAAE,IAAI,CAAC,mBAAmB,CAAC;QAChC,SAAS,EAAE,IAAI,CAAC,wBAAwB,CAAC;QACzC,IAAI,EAAE,IAAI,CAAC,mBAAmB,CAAC;QAC/B,MAAM,EAAE,IAAI,CAAC,qBAAqB,CAAC;QACnC,KAAK,EAAE,IAAI,CAAC,oBAAoB,CAAC;QACjC,yDAAyD;QACzD,SAAS,EAAE,IAAI,CAAC,yBAAyB,CAAC;QAC1C,QAAQ,EAAE,IAAI,CAAC,wBAAwB,CAAC;QACxC,SAAS,EAAE,IAAI,CAAC,wBAAwB,CAAC;KAC1C;IACD;;;OAGG;IACH,GAAG,EAAE;QACH,QAAQ,EAAE,kBAAkB;QAC5B,SAAS,EAAE,aAAa;QACxB,UAAU,EAAE,YAAY;QACxB,aAAa,EAAE,aAAa;QAC5B,MAAM,EAAE,YAAY;KACrB;CACO,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,MAAM,EAAE,IAAI,CAAC,gBAAgB,CAAC;IAC9B,MAAM,EAAE,IAAI,CAAC,uBAAuB,CAAC;IACrC,KAAK,EAAE,IAAI,CAAC,eAAe,CAAC;IAC5B,OAAO,EAAE,IAAI,CAAC,wBAAwB,CAAC;IACvC,uEAAuE;IACvE,MAAM,EAAE;QACN,KAAK,EAAE,0CAA0C;QACjD,IAAI,EAAE,yBAAyB;QAC/B,0DAA0D;QAC1D,SAAS,EAAE,aAAa;QACxB,2EAA2E;QAC3E,OAAO,EAAE,WAAW;KACrB;IACD;;;;OAIG;IACH,MAAM,EAAE;QACN,GAAG,EAAE,kBAAkB;QACvB,SAAS,EAAE,aAAa;QACxB,KAAK,EAAE,YAAY;QACnB,QAAQ,EAAE,aAAa;QACvB,MAAM,EAAE,YAAY;KACrB;CACO,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,SAAS,EAAE,YAAY;IACvB,UAAU,EAAE,eAAe;IAC3B,uEAAuE;IACvE,eAAe,EAAE,mBAAmB;IACpC,iBAAiB,EAAE,qBAAqB;CAChC,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { type ProtocolSuiteOptions } from './types.js';
2
+ /**
3
+ * The six end-to-end checks §23.4 requires, written once.
4
+ *
5
+ * HOW-TO §23.4 layer 3 names the same six things for every extension, none of
6
+ * which exist until something is deployed and none of which a unit test can
7
+ * see: the login page, the extension inside the CortenaWeb frame, the MCP
8
+ * consent page, the agent pop-up, UI state, and shell chrome. Written per
9
+ * extension that is six copies of the same fixtures and six ways of finding
10
+ * the sidebar — and drift on the one layer that is supposed to prove the fleet
11
+ * looks the same. Here it is a config file per extension, and a renamed
12
+ * `data-slot` fails every adopter's suite in `locators.ts` rather than in six
13
+ * places.
14
+ *
15
+ * // extensions/<name>/e2e/protocol.spec.ts
16
+ * defineProtocolSuite({
17
+ * host: 'https://test.<name>.ascendenceai.com',
18
+ * listRoute: '/things',
19
+ * filter: { param: 'status', value: 'open' },
20
+ * detail: { open, urlPattern, close },
21
+ * agentSlug: '<name>',
22
+ * helpTopic: 'Things',
23
+ * expects: { brandName: 'Things', navItems: [...], listHeading: /things/i, agentPrompt: '...' },
24
+ * cases: { login, embedded, consent, popup, listState, shell },
25
+ * });
26
+ *
27
+ * Both themes are the two Playwright projects `protocolConfig()` defines, so
28
+ * every one of the six runs twice and the run's variant is read off the project
29
+ * name by the Assure reporter. `retries: 0`, because a flaky test is a defect.
30
+ */
31
+ export declare function defineProtocolSuite(options: ProtocolSuiteOptions): void;
@@ -0,0 +1,196 @@
1
+ import { expect, test } from '@playwright/test';
2
+ import { assureAnnotation, isAssureAnnotation, } from '@ascendenceai/cortena-extensions-assure-reporters/annotations';
3
+ import { consent } from '../fixtures/consent.js';
4
+ import { embedded, expectTokenStrippedFromUrl, postHostTheme } from '../fixtures/embedded.js';
5
+ import { listState } from '../fixtures/list-state.js';
6
+ import { popup, expectSessionResumes } from '../fixtures/popup.js';
7
+ import { expectShellChrome, shell } from '../fixtures/shell.js';
8
+ import { isSignInScreen, organisationStep, signIn } from '../fixtures/sign-in.js';
9
+ import { expectTheme, theme, themeFromProjectName } from '../fixtures/theme.js';
10
+ import { SHELL } from '../locators.js';
11
+ import { readCredentials } from '../env.js';
12
+ import { PROTOCOL_CASE_KEYS } from './types.js';
13
+ /**
14
+ * The six end-to-end checks §23.4 requires, written once.
15
+ *
16
+ * HOW-TO §23.4 layer 3 names the same six things for every extension, none of
17
+ * which exist until something is deployed and none of which a unit test can
18
+ * see: the login page, the extension inside the CortenaWeb frame, the MCP
19
+ * consent page, the agent pop-up, UI state, and shell chrome. Written per
20
+ * extension that is six copies of the same fixtures and six ways of finding
21
+ * the sidebar — and drift on the one layer that is supposed to prove the fleet
22
+ * looks the same. Here it is a config file per extension, and a renamed
23
+ * `data-slot` fails every adopter's suite in `locators.ts` rather than in six
24
+ * places.
25
+ *
26
+ * // extensions/<name>/e2e/protocol.spec.ts
27
+ * defineProtocolSuite({
28
+ * host: 'https://test.<name>.ascendenceai.com',
29
+ * listRoute: '/things',
30
+ * filter: { param: 'status', value: 'open' },
31
+ * detail: { open, urlPattern, close },
32
+ * agentSlug: '<name>',
33
+ * helpTopic: 'Things',
34
+ * expects: { brandName: 'Things', navItems: [...], listHeading: /things/i, agentPrompt: '...' },
35
+ * cases: { login, embedded, consent, popup, listState, shell },
36
+ * });
37
+ *
38
+ * Both themes are the two Playwright projects `protocolConfig()` defines, so
39
+ * every one of the six runs twice and the run's variant is read off the project
40
+ * name by the Assure reporter. `retries: 0`, because a flaky test is a defect.
41
+ */
42
+ export function defineProtocolSuite(options) {
43
+ assertCases(options);
44
+ const { expects, listRoute, agentSlug } = options;
45
+ test.describe('protocol (§23.4, P-36)', () => {
46
+ /*
47
+ * The theme is applied before every navigation, not inside the specs: the
48
+ * stored choice is read before the first paint, so a spec that sets it
49
+ * after `goto` has proved a re-render and nothing else.
50
+ */
51
+ test.beforeEach(async ({ page }, testInfo) => {
52
+ await theme(page, themeFromProjectName(testInfo.project.name));
53
+ });
54
+ test('the login page asks for the organisation and signs a person in', { annotation: [assureAnnotation(options.cases.login)] }, async ({ page }, testInfo) => {
55
+ const credentials = readCredentials();
56
+ test.skip(!credentials.email || !credentials.password, 'CORTENA_E2E_USER_EMAIL and CORTENA_E2E_USER_PASSWORD are needed: a token proves the '
57
+ + 'handoff, only the form proves a person can sign in.');
58
+ await page.goto('/');
59
+ /*
60
+ * Signed out, the app must *be* a sign-in screen rather than an empty
61
+ * shell or a spinner. §10.4.1: whatever happened, the user lands
62
+ * somewhere that says what to do next.
63
+ */
64
+ expect(await isSignInScreen(page), 'a signed-out visit must land on the sign-in screen').toBe(true);
65
+ await expectTheme(page, themeFromProjectName(testInfo.project.name));
66
+ // The organisation step decides *how* this org signs in before it asks
67
+ // for anything — password, Google, or both (P-08). `signIn` walks it
68
+ // again from the top, which is the flow a person actually takes.
69
+ await organisationStep(page, credentials.orgId);
70
+ await signIn(page, { useForm: true, credentials, route: '/' });
71
+ await expect(shell(page).root).toBeVisible();
72
+ });
73
+ test('a token-only launch renders the extension, not the login page', { annotation: [assureAnnotation(options.cases.embedded)] }, async ({ page }, testInfo) => {
74
+ const mode = themeFromProjectName(testInfo.project.name);
75
+ await embedded(page, { route: listRoute });
76
+ await expectTokenStrippedFromUrl(page);
77
+ await expectListRendered(page, expects.listHeading);
78
+ /*
79
+ * The host owns the theme in the frame: CortenaWeb posts it in on load
80
+ * and on every change, and the extension applies the *mode* it implies.
81
+ *
82
+ * The **opposite** mode first, and that is the whole assertion. The
83
+ * page already carries `mode` from `beforeEach`, so posting `mode` and
84
+ * asserting `mode` is a tautology — delete cortena-ui's `cortena-theme`
85
+ * listener entirely and it still passes. Posting the opposite means the
86
+ * attribute can only change if the message was received, and posting
87
+ * back leaves the page in the theme the project is named for.
88
+ */
89
+ const opposite = mode === 'dark' ? 'light' : 'dark';
90
+ await postHostTheme(page, opposite);
91
+ await postHostTheme(page, mode);
92
+ });
93
+ test('the MCP consent page names the resource and can be refused', { annotation: [assureAnnotation(options.cases.consent)] }, async ({ page }) => {
94
+ await consent(page, {
95
+ host: options.host,
96
+ scopes: expects.consentScopes,
97
+ // A client registered by hand, when an environment forbids dynamic
98
+ // registration. Absent, one is registered the way an MCP client does.
99
+ clientId: process.env.CORTENA_E2E_MCP_CLIENT_ID?.trim() || undefined,
100
+ });
101
+ });
102
+ test('the agent pop-up opens, streams a turn, resumes it after a reload and draws A2UI', { annotation: [assureAnnotation(options.cases.popup)] }, async ({ page }) => {
103
+ await embedded(page, { route: listRoute });
104
+ const chat = popup(page);
105
+ await chat.open();
106
+ await chat.ask(expects.agentPrompt);
107
+ await expect(chat.assistantMessages.first()).toBeVisible();
108
+ // The block of A2UI, which is the half of P-25 that says the agent can
109
+ // answer with a screen rather than with a paragraph about one.
110
+ const a2uiPrompt = expects.a2uiPrompt;
111
+ if (a2uiPrompt) {
112
+ await chat.ask(a2uiPrompt);
113
+ await expect(chat.a2ui.or(chat.toolCards).first()).toBeVisible();
114
+ }
115
+ // The session is the server's, not this tab's.
116
+ await expectSessionResumes(page, expects.agentPrompt);
117
+ });
118
+ test('the list keeps its filter through the detail, Back, reload and a new tab', { annotation: [assureAnnotation(options.cases.listState)] }, async ({ page }) => {
119
+ // Signed in first, so the five navigations below are the list's own and
120
+ // not a redirect to the login screen five times.
121
+ await embedded(page, { route: listRoute });
122
+ await expectListRendered(page, expects.listHeading);
123
+ await listState(page, {
124
+ route: listRoute,
125
+ filter: options.filter,
126
+ detail: options.detail,
127
+ });
128
+ });
129
+ test('the shell puts the mark top-left, Settings under the avatar, theme then help bottom-right', { annotation: [assureAnnotation(options.cases.shell)] }, async ({ page }, testInfo) => {
130
+ const mode = themeFromProjectName(testInfo.project.name);
131
+ await embedded(page, { route: listRoute });
132
+ await expectShellChrome(page, expects);
133
+ await expectTheme(page, mode);
134
+ /*
135
+ * §10.1 and §19.1: the mark id, the extension id and the AgentTemplate
136
+ * slug are **one value**. `data-mark` reads `fallback` when nothing is
137
+ * registered for that id, which renders an initial in a box — so this
138
+ * catches both a mark nobody registered and a slug that drifted from
139
+ * the id the shell was given.
140
+ */
141
+ await expect(shell(page).brandMark).toHaveAttribute(SHELL.markAttribute, agentSlug);
142
+ // The help panel answers from the extension's own functional document
143
+ // (§13, P-13) — not a hand-written copy that drifts.
144
+ const s = shell(page);
145
+ await s.openHelp();
146
+ await expect(s.helpPanel).toContainText(options.helpTopic);
147
+ await page.keyboard.press('Escape');
148
+ // The drawer is the same navigation below the breakpoint, with a focus
149
+ // trap and a way out. A sidebar that simply disappears on a phone is
150
+ // the failure this catches.
151
+ await s.openNavDrawer();
152
+ await expect(s.navDrawer.locator(SHELL.navItem).first()).toBeVisible();
153
+ await s.navDrawerClose.click();
154
+ await expect(s.navDrawer).toHaveCount(0);
155
+ /*
156
+ * The other way into the same theme, in a second page of the same
157
+ * context. `data-theme` absent means cortena-design falls back to
158
+ * `prefers-color-scheme`, and a suite that only ever writes the
159
+ * attribute never exercises the path a first-time visitor takes — so
160
+ * an extension can be correct in dark mode only for people who once
161
+ * pressed the toggle. It is a second page rather than a seventh spec
162
+ * because it is the same claim about the same screen.
163
+ */
164
+ const osPage = await page.context().newPage();
165
+ try {
166
+ await theme(osPage, mode, { path: 'os' });
167
+ await embedded(osPage, { route: listRoute });
168
+ await expectTheme(osPage, mode, 'os');
169
+ }
170
+ finally {
171
+ await osPage.close();
172
+ }
173
+ });
174
+ });
175
+ }
176
+ /** The list route rendered, by the name a person reads rather than a test id. */
177
+ async function expectListRendered(page, heading) {
178
+ await expect(page.getByRole('heading', { name: heading }).first()).toBeVisible();
179
+ }
180
+ /**
181
+ * Six cases, all six present and all six real.
182
+ *
183
+ * A missing one used to be a spec that ran and recorded nothing, which is the
184
+ * exact shape of failure the Assure reporters exist to prevent: a green
185
+ * pipeline over an empty record.
186
+ */
187
+ function assertCases(options) {
188
+ const missing = PROTOCOL_CASE_KEYS.filter((key) => !isAssureAnnotation(options.cases?.[key]));
189
+ if (missing.length) {
190
+ throw new Error(`defineProtocolSuite: the cases ${missing.join(', ')} are missing or are not assureCase() `
191
+ + 'literals. Write all six out in this file — assure-sync-cases parses the literal without '
192
+ + 'running the test and never looks inside node_modules, so a case built in the kit is a '
193
+ + 'case the catalogue never hears about. Copy them from the kit README.');
194
+ }
195
+ }
196
+ //# sourceMappingURL=suite.js.map