ui: add frontend diagnostics and stable test hooks
This commit is contained in:
@@ -2,18 +2,18 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/frontend-observability-and-testability`
|
||||
### `feat/id-display-support`
|
||||
|
||||
Status: in_progress
|
||||
|
||||
DoD:
|
||||
- UI modules expose stable hooks for diagnostics and targeted testing where the current DOM is ambiguous
|
||||
- console/runtime failures on critical pages are easier to attribute to a concrete page module
|
||||
- existing UI e2e coverage stays green after the observability/testability pass
|
||||
- `define_id!` implements `Display`
|
||||
- formatting with typed ids no longer requires pervasive `.as_str()`
|
||||
- the workspace builds cleanly after the macro change
|
||||
|
||||
## Next
|
||||
|
||||
- `feat/id-display-support`
|
||||
- `feat/secret-crypto-hkdf`
|
||||
|
||||
## Backlog
|
||||
|
||||
|
||||
+12
-12
@@ -52,7 +52,7 @@
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name">workspace</span>
|
||||
<span class="ws-current-name" id="ws-current-name" data-testid="shell-workspace-name">workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" style="display:none">
|
||||
@@ -81,9 +81,9 @@
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications"><svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg></button>
|
||||
<div class="nav-divider"></div>
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="nav-avatar" data-testid="shell-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header"><div class="user-dropdown-name">Crank</div><div class="user-dropdown-role">—</div></div>
|
||||
<div class="user-dropdown-header"><div class="user-dropdown-name" data-testid="shell-user-name">Crank</div><div class="user-dropdown-role" data-testid="shell-user-role">—</div></div>
|
||||
<button class="user-dropdown-item" data-action="settings-link"><svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg><span data-i18n="nav.settings">Settings</span></button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" data-action="logout"><svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg><span data-i18n="nav.logout">Log out</span></button>
|
||||
@@ -108,7 +108,7 @@
|
||||
<p class="page-subtitle" data-i18n="secrets.subtitle">Encrypted upstream credentials for bearer tokens, basic auth, and API keys used by auth profiles.</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" id="btn-create-secret" type="button">
|
||||
<button class="btn-primary" id="btn-create-secret" data-testid="secret-create-button" type="button">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
<span data-i18n="secrets.new">Create secret</span>
|
||||
</button>
|
||||
@@ -168,7 +168,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="secret-modal">
|
||||
<div class="modal-overlay" id="secret-modal" data-testid="secret-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="secret-modal-title" data-i18n="secrets.modal.create_title">Create secret</span>
|
||||
@@ -181,13 +181,13 @@
|
||||
<div class="modal-body">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="secrets.modal.name">Secret name</label>
|
||||
<input class="field-input" id="secret-name" type="text" autocomplete="off">
|
||||
<input class="field-input" id="secret-name" data-testid="secret-name-input" type="text" autocomplete="off">
|
||||
<div class="field-hint" data-i18n="secrets.modal.name_hint">Use a stable operator-facing label. Plaintext values are never returned after storage.</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="secrets.modal.kind">Secret kind</label>
|
||||
<select class="field-select" id="secret-kind">
|
||||
<select class="field-select" id="secret-kind" data-testid="secret-kind-select">
|
||||
<option value="token" data-i18n="secrets.kind.token">Token</option>
|
||||
<option value="username_password" data-i18n="secrets.kind.username_password">Username/password</option>
|
||||
<option value="header" data-i18n="secrets.kind.header">Header value</option>
|
||||
@@ -199,23 +199,23 @@
|
||||
<div class="secret-value-grid" id="secret-value-fields">
|
||||
<div class="field-group" data-kind-field="string">
|
||||
<label class="field-label" id="secret-string-label" data-i18n="secrets.modal.value">Secret value</label>
|
||||
<input class="field-input" id="secret-string-value" type="text" autocomplete="off">
|
||||
<input class="field-input" id="secret-string-value" data-testid="secret-value-input" type="text" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="field-row" data-kind-field="basic" style="display:none;">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="secrets.modal.username">Username</label>
|
||||
<input class="field-input" id="secret-username" type="text" autocomplete="off">
|
||||
<input class="field-input" id="secret-username" data-testid="secret-username-input" type="text" autocomplete="off">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="secrets.modal.password">Password</label>
|
||||
<input class="field-input" id="secret-password" type="password" autocomplete="new-password">
|
||||
<input class="field-input" id="secret-password" data-testid="secret-password-input" type="password" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group" data-kind-field="json" style="display:none;">
|
||||
<label class="field-label" data-i18n="secrets.modal.json_payload">JSON payload</label>
|
||||
<textarea class="field-textarea code-textarea" id="secret-json-value" rows="8" spellcheck="false"></textarea>
|
||||
<textarea class="field-textarea code-textarea" id="secret-json-value" data-testid="secret-json-input" rows="8" spellcheck="false"></textarea>
|
||||
<div class="field-hint" data-i18n="secrets.modal.json_hint">Useful for generic secret payloads. The value must be valid JSON.</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,7 +224,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" id="secret-modal-cancel-btn" type="button" data-i18n="secrets.modal.cancel">Cancel</button>
|
||||
<button class="btn-primary" id="secret-modal-submit-btn" type="button" data-i18n="secrets.modal.create_action">Create secret</button>
|
||||
<button class="btn-primary" id="secret-modal-submit-btn" data-testid="secret-submit-button" type="button" data-i18n="secrets.modal.create_action">Create secret</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name">acme-workspace</span>
|
||||
<span class="ws-current-name" id="ws-current-name" data-testid="shell-workspace-name">acme-workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" style="display:none">
|
||||
@@ -59,11 +59,11 @@
|
||||
<div class="nav-divider"></div>
|
||||
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="nav-avatar" data-testid="shell-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header">
|
||||
<div class="user-dropdown-name">Crank</div>
|
||||
<div class="user-dropdown-role">—</div>
|
||||
<div class="user-dropdown-name" data-testid="shell-user-name">Crank</div>
|
||||
<div class="user-dropdown-role" data-testid="shell-user-role">—</div>
|
||||
</div>
|
||||
<button class="user-dropdown-item" data-action="settings-link">
|
||||
<svg width="13" height="13"><use href="../../icons/general/settings.svg#icon"/></svg>
|
||||
@@ -226,7 +226,7 @@
|
||||
|
||||
</div><!-- /wizard-shell -->
|
||||
|
||||
<div class="modal-overlay" id="quick-secret-modal">
|
||||
<div class="modal-overlay" id="quick-secret-modal" data-testid="wizard-quick-secret-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" data-i18n="wizard.step2.quick_secret_title">Quick create secret</span>
|
||||
@@ -239,11 +239,11 @@
|
||||
<div class="modal-body" style="display:grid; gap:14px;">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="wizard.step2.quick_secret_name">Secret name</label>
|
||||
<input class="field-input" id="quick-secret-name" type="text" autocomplete="off">
|
||||
<input class="field-input" id="quick-secret-name" data-testid="wizard-quick-secret-name" type="text" autocomplete="off">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="wizard.step2.quick_secret_kind">Secret kind</label>
|
||||
<select class="field-select" id="quick-secret-kind">
|
||||
<select class="field-select" id="quick-secret-kind" data-testid="wizard-quick-secret-kind">
|
||||
<option value="token" data-i18n="secrets.kind.token">Token</option>
|
||||
<option value="header" data-i18n="secrets.kind.header">Header value</option>
|
||||
<option value="generic" data-i18n="secrets.kind.generic">Generic JSON</option>
|
||||
@@ -251,13 +251,13 @@
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="wizard.step2.quick_secret_value">Secret value</label>
|
||||
<input class="field-input" id="quick-secret-value" type="text" autocomplete="off">
|
||||
<input class="field-input" id="quick-secret-value" data-testid="wizard-quick-secret-value" type="text" autocomplete="off">
|
||||
<div class="field-hint" data-i18n="wizard.step2.quick_secret_hint">The plaintext is encrypted immediately and will not be returned again.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" id="quick-secret-cancel-btn" type="button" data-i18n="btn.cancel">Cancel</button>
|
||||
<button class="btn-primary" id="quick-secret-submit-btn" type="button" data-i18n="wizard.step2.quick_secret_submit">Create secret</button>
|
||||
<button class="btn-primary" id="quick-secret-submit-btn" data-testid="wizard-quick-secret-submit" type="button" data-i18n="wizard.step2.quick_secret_submit">Create secret</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Protocol selection cards -->
|
||||
<div class="protocol-grid">
|
||||
<div class="protocol-grid" data-testid="wizard-protocol-grid">
|
||||
|
||||
<div class="protocol-card rest selected" data-protocol="rest" role="radio" aria-checked="true" tabindex="0">
|
||||
<div class="protocol-card rest selected" data-protocol="rest" data-testid="wizard-protocol-rest" role="radio" aria-checked="true" tabindex="0">
|
||||
<div class="protocol-card-check">
|
||||
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protocol-card graphql" data-protocol="graphql" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card graphql" data-protocol="graphql" data-testid="wizard-protocol-graphql" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card-check">
|
||||
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protocol-card grpc" data-protocol="grpc" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card grpc" data-protocol="grpc" data-testid="wizard-protocol-grpc" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card-check">
|
||||
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protocol-card websocket" data-protocol="websocket" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card websocket" data-protocol="websocket" data-testid="wizard-protocol-websocket" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card-check">
|
||||
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protocol-card soap" data-protocol="soap" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card soap" data-protocol="soap" data-testid="wizard-protocol-soap" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card-check">
|
||||
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
||||
</div>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.auth_selector">Upstream auth</label>
|
||||
<select class="form-select" id="new-upstream-auth-mode" onchange="updateUpstreamAuthUi()">
|
||||
<select class="form-select" id="new-upstream-auth-mode" data-testid="wizard-auth-mode-select" onchange="updateUpstreamAuthUi()">
|
||||
<option value="none" data-i18n="wizard.step2.auth_mode.none">No auth</option>
|
||||
<option value="existing" data-i18n="wizard.step2.auth_mode.existing">Use existing auth profile</option>
|
||||
<option value="create" data-i18n="wizard.step2.auth_mode.create">Create auth profile now</option>
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
<div class="form-group" id="upstream-auth-existing-group" style="display:none;">
|
||||
<label class="form-label" data-i18n="wizard.step2.auth_profile">Auth profile</label>
|
||||
<select class="form-select" id="new-upstream-auth-profile"></select>
|
||||
<select class="form-select" id="new-upstream-auth-profile" data-testid="wizard-auth-profile-select"></select>
|
||||
<div class="form-hint" id="new-upstream-auth-profile-hint" data-i18n="wizard.step2.auth_profile_hint">Selected profile will be resolved at runtime and attached through `execution_config.auth_profile_ref`.</div>
|
||||
</div>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<div class="form-row" style="grid-template-columns: 1fr 1fr;">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><span data-i18n="wizard.step2.profile_kind">Auth kind</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<select class="form-select" id="new-auth-profile-kind" onchange="updateAuthProfileCreateUi()">
|
||||
<select class="form-select" id="new-auth-profile-kind" data-testid="wizard-auth-profile-kind-select" onchange="updateAuthProfileCreateUi()">
|
||||
<option value="bearer" data-i18n="wizard.step2.auth_kind.bearer">Bearer token</option>
|
||||
<option value="basic" data-i18n="wizard.step2.auth_kind.basic">Basic auth</option>
|
||||
<option value="api_key_header" data-i18n="wizard.step2.auth_kind.api_key_header">API key header</option>
|
||||
@@ -137,19 +137,19 @@
|
||||
<div class="form-row" id="auth-profile-basic-secret-row" style="display:none; grid-template-columns: 1fr 1fr;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.username_secret">Username secret</label>
|
||||
<select class="form-select" id="new-auth-profile-username-secret"></select>
|
||||
<select class="form-select" id="new-auth-profile-username-secret" data-testid="wizard-auth-username-secret-select"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.password_secret">Password secret</label>
|
||||
<select class="form-select" id="new-auth-profile-password-secret"></select>
|
||||
<select class="form-select" id="new-auth-profile-password-secret" data-testid="wizard-auth-password-secret-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="auth-profile-single-secret-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.secret_value">Secret</label>
|
||||
<select class="form-select" id="new-auth-profile-secret-id"></select>
|
||||
<select class="form-select" id="new-auth-profile-secret-id" data-testid="wizard-auth-secret-select"></select>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||||
<button class="btn-ghost-sm" onclick="openQuickSecretModal(event)" data-i18n="wizard.step2.quick_secret">Quick create secret</button>
|
||||
<button class="btn-ghost-sm" data-testid="wizard-open-quick-secret" onclick="openQuickSecretModal(event)" data-i18n="wizard.step2.quick_secret">Quick create secret</button>
|
||||
<a class="btn-ghost-sm" href="/secrets" target="_blank" rel="noopener" data-i18n="wizard.step2.manage_secrets">Open secrets page</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
-5
@@ -135,14 +135,14 @@
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tab-row">
|
||||
<template x-for="t in tabDefs" :key="t.id">
|
||||
<template x-for="tabDef in tabDefs" :key="tabDef.id">
|
||||
<button
|
||||
class="tab-item"
|
||||
:class="{ active: tab === t.id }"
|
||||
@click="setTab(t.id)"
|
||||
:class="{ active: tab === tabDef.id }"
|
||||
@click="setTab(tabDef.id)"
|
||||
>
|
||||
<span x-text="t.label"></span>
|
||||
<span class="tab-count" x-text="tabCount(t.id)"></span>
|
||||
<span x-text="tabDef.label"></span>
|
||||
<span class="tab-count" x-text="tabCount(tabDef.id)"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -139,6 +139,9 @@
|
||||
document.querySelectorAll('.user-dropdown-role').forEach(function(element) {
|
||||
element.textContent = role + ' · ' + workspace;
|
||||
});
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.ensureShellTestIds === 'function') {
|
||||
window.CrankDiagnostics.ensureShellTestIds();
|
||||
}
|
||||
}
|
||||
|
||||
function replaceSession(session) {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
(function() {
|
||||
var currentPage = '';
|
||||
|
||||
function messageFor(error) {
|
||||
if (!error) {
|
||||
return 'unknown error';
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function setPage(pageName) {
|
||||
currentPage = pageName || '';
|
||||
document.documentElement.setAttribute('data-crank-page', currentPage || 'unknown');
|
||||
}
|
||||
|
||||
function markBootstrapState(pageName, status, details) {
|
||||
document.documentElement.setAttribute('data-crank-bootstrap-page', pageName || currentPage || 'unknown');
|
||||
document.documentElement.setAttribute('data-crank-bootstrap-state', status);
|
||||
if (status === 'error' && details) {
|
||||
document.documentElement.setAttribute('data-crank-bootstrap-error', details);
|
||||
return;
|
||||
}
|
||||
document.documentElement.removeAttribute('data-crank-bootstrap-error');
|
||||
}
|
||||
|
||||
function report(stage, error, pageName) {
|
||||
var resolvedPage = pageName || currentPage || 'unknown';
|
||||
console.error('[CrankUI][' + resolvedPage + '][' + stage + ']', error);
|
||||
window.dispatchEvent(new CustomEvent('crank:ui-error', {
|
||||
detail: {
|
||||
page: resolvedPage,
|
||||
stage: stage,
|
||||
message: messageFor(error),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function bootstrap(pageName, init) {
|
||||
function run() {
|
||||
setPage(pageName);
|
||||
markBootstrapState(pageName, 'pending');
|
||||
try {
|
||||
var result = init();
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function() {
|
||||
markBootstrapState(pageName, 'ready');
|
||||
}).catch(function(error) {
|
||||
markBootstrapState(pageName, 'error', messageFor(error));
|
||||
report('bootstrap', error, pageName);
|
||||
});
|
||||
return;
|
||||
}
|
||||
markBootstrapState(pageName, 'ready');
|
||||
} catch (error) {
|
||||
markBootstrapState(pageName, 'error', messageFor(error));
|
||||
report('bootstrap', error, pageName);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run, { once: true });
|
||||
return;
|
||||
}
|
||||
run();
|
||||
}
|
||||
|
||||
function ensureTestId(element, value) {
|
||||
if (!element || !value) {
|
||||
return;
|
||||
}
|
||||
element.setAttribute('data-testid', value);
|
||||
}
|
||||
|
||||
function ensureShellTestIds() {
|
||||
document.querySelectorAll('.nav-avatar').forEach(function(element) {
|
||||
ensureTestId(element, 'shell-avatar');
|
||||
});
|
||||
document.querySelectorAll('.user-dropdown-name').forEach(function(element) {
|
||||
ensureTestId(element, 'shell-user-name');
|
||||
});
|
||||
document.querySelectorAll('.user-dropdown-role').forEach(function(element) {
|
||||
ensureTestId(element, 'shell-user-role');
|
||||
});
|
||||
ensureTestId(document.getElementById('ws-current-name'), 'shell-workspace-name');
|
||||
}
|
||||
|
||||
window.addEventListener('error', function(event) {
|
||||
if (!currentPage) {
|
||||
return;
|
||||
}
|
||||
report('runtime', event.error || new Error(event.message || 'window error'));
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
if (!currentPage) {
|
||||
return;
|
||||
}
|
||||
report('unhandledrejection', event.reason || new Error('unhandled rejection'));
|
||||
});
|
||||
|
||||
window.CrankDiagnostics = {
|
||||
bootstrap: bootstrap,
|
||||
ensureTestId: ensureTestId,
|
||||
ensureShellTestIds: ensureShellTestIds,
|
||||
report: report,
|
||||
setPage: setPage,
|
||||
};
|
||||
}());
|
||||
+11
-3
@@ -14,9 +14,11 @@
|
||||
errorElement.style.display = 'none';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
function initLoginPage() {
|
||||
window.CrankAuth.guardLoginPage().catch(function(error) {
|
||||
console.error(error);
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.report === 'function') {
|
||||
window.CrankDiagnostics.report('guard-login-page', error, 'login');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', async function(event) {
|
||||
@@ -42,5 +44,11 @@
|
||||
showError(tKey('login.error.generic'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
|
||||
window.CrankDiagnostics.bootstrap('login', initLoginPage);
|
||||
return;
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', initLoginPage);
|
||||
}());
|
||||
|
||||
+15
-2
@@ -1,4 +1,4 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
function initSecretsPage() {
|
||||
var state = {
|
||||
workspaceId: null,
|
||||
secrets: [],
|
||||
@@ -145,6 +145,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
updateKindFields();
|
||||
modal.classList.add('open');
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.ensureTestId === 'function') {
|
||||
window.CrankDiagnostics.ensureTestId(modal, mode === 'rotate' ? 'secret-rotate-modal' : 'secret-create-modal');
|
||||
}
|
||||
setTimeout(function () {
|
||||
if (mode === 'rotate') {
|
||||
stringField.focus();
|
||||
@@ -295,6 +298,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
var rotateButton = document.createElement('button');
|
||||
rotateButton.className = 'icon-btn';
|
||||
rotateButton.textContent = tKey('secrets.action.rotate');
|
||||
rotateButton.setAttribute('data-testid', 'secret-rotate-action');
|
||||
rotateButton.setAttribute('data-secret-id', secret.id);
|
||||
rotateButton.addEventListener('click', function () {
|
||||
openModal('rotate', secret);
|
||||
});
|
||||
@@ -303,6 +308,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
var deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'icon-btn danger';
|
||||
deleteButton.textContent = tKey('secrets.action.delete');
|
||||
deleteButton.setAttribute('data-testid', 'secret-delete-action');
|
||||
deleteButton.setAttribute('data-secret-id', secret.id);
|
||||
deleteButton.addEventListener('click', async function () {
|
||||
await deleteSecret(secret);
|
||||
});
|
||||
@@ -560,4 +567,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
updateKindFields();
|
||||
void load();
|
||||
});
|
||||
}
|
||||
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
|
||||
window.CrankDiagnostics.bootstrap('secrets', initSecretsPage);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', initSecretsPage);
|
||||
}
|
||||
|
||||
+10
-2
@@ -35,7 +35,7 @@ var editingUpstreamId = window.editingUpstreamId;
|
||||
var protoParsed = window.protoParsed;
|
||||
var selectedRpcMethod = window.selectedRpcMethod;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
async function initWizardPage() {
|
||||
document.querySelector('.btn-continue').addEventListener('click', function() {
|
||||
if (currentStep < TOTAL_STEPS) {
|
||||
goToStep(currentStep + 1);
|
||||
@@ -128,7 +128,15 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
|
||||
updateWizardProtocolVisibility();
|
||||
_doGoToStep(1);
|
||||
});
|
||||
}
|
||||
|
||||
if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'function') {
|
||||
window.CrankDiagnostics.bootstrap('wizard', initWizardPage);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
void initWizardPage();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* ── HTTP method picker (Step 4 REST) ── */
|
||||
|
||||
@@ -17,6 +17,7 @@ const BUNDLES = {
|
||||
'js/config.js',
|
||||
'js/i18n.js',
|
||||
'js/dom.js',
|
||||
'js/diagnostics.js',
|
||||
'js/workspace.js',
|
||||
'js/api.js',
|
||||
'js/ui-feedback.js',
|
||||
@@ -28,6 +29,7 @@ const BUNDLES = {
|
||||
files: [
|
||||
'js/config.js',
|
||||
'js/i18n.js',
|
||||
'js/diagnostics.js',
|
||||
'js/api.js',
|
||||
'js/auth.js',
|
||||
'js/login.js',
|
||||
@@ -89,6 +91,7 @@ const BUNDLES = {
|
||||
},
|
||||
'workspace-setup': {
|
||||
files: [
|
||||
'js/diagnostics.js',
|
||||
'js/workspace-setup.js',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { login } = require('./helpers');
|
||||
|
||||
test('shell and wizard expose stable diagnostics hooks', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.locator('[data-testid="shell-avatar"]')).toBeVisible();
|
||||
await page.locator('[data-testid="shell-avatar"]').click();
|
||||
await expect(page.locator('[data-testid="shell-user-name"]').first()).toBeVisible();
|
||||
await expect(page.locator('[data-testid="shell-user-role"]').first()).toBeVisible();
|
||||
|
||||
await page.goto('/wizard/');
|
||||
await expect(page.locator('[data-testid="wizard-protocol-grid"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeVisible();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-crank-bootstrap-state', 'ready');
|
||||
});
|
||||
|
||||
test('secrets page exposes stable secret management hooks', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/secrets');
|
||||
|
||||
await expect(page.locator('[data-testid="secret-create-button"]')).toBeVisible();
|
||||
await page.locator('[data-testid="secret-create-button"]').click();
|
||||
await expect(page.locator('[data-testid="secret-create-modal"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="secret-name-input"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="secret-kind-select"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="secret-submit-button"]')).toBeVisible();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-crank-bootstrap-state', 'ready');
|
||||
});
|
||||
Reference in New Issue
Block a user