@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.
- package/LICENSE +7 -0
- package/README.md +126 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +63 -0
- package/dist/config.js.map +1 -0
- package/dist/env.d.ts +43 -0
- package/dist/env.js +67 -0
- package/dist/env.js.map +1 -0
- package/dist/fixtures/consent.d.ts +107 -0
- package/dist/fixtures/consent.js +220 -0
- package/dist/fixtures/consent.js.map +1 -0
- package/dist/fixtures/embedded.d.ts +47 -0
- package/dist/fixtures/embedded.js +60 -0
- package/dist/fixtures/embedded.js.map +1 -0
- package/dist/fixtures/list-state.d.ts +43 -0
- package/dist/fixtures/list-state.js +58 -0
- package/dist/fixtures/list-state.js.map +1 -0
- package/dist/fixtures/popup.d.ts +44 -0
- package/dist/fixtures/popup.js +81 -0
- package/dist/fixtures/popup.js.map +1 -0
- package/dist/fixtures/shell.d.ts +63 -0
- package/dist/fixtures/shell.js +96 -0
- package/dist/fixtures/shell.js.map +1 -0
- package/dist/fixtures/sign-in.d.ts +59 -0
- package/dist/fixtures/sign-in.js +115 -0
- package/dist/fixtures/sign-in.js.map +1 -0
- package/dist/fixtures/theme.d.ts +42 -0
- package/dist/fixtures/theme.js +79 -0
- package/dist/fixtures/theme.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/locators.d.ts +210 -0
- package/dist/locators.js +212 -0
- package/dist/locators.js.map +1 -0
- package/dist/protocol/suite.d.ts +31 -0
- package/dist/protocol/suite.js +196 -0
- package/dist/protocol/suite.js.map +1 -0
- package/dist/protocol/types.d.ts +61 -0
- package/dist/protocol/types.js +4 -0
- package/dist/protocol/types.js.map +1 -0
- 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
|
+
};
|
package/dist/locators.js
ADDED
|
@@ -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":"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"}
|
|
@@ -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
|