mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-25 04:33:03 +00:00
test(cloudcp): cover Pulse Account render modules
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
renderDeletePanel,
|
||||
renderExportPanel,
|
||||
renderExportResult,
|
||||
renderManagePanel,
|
||||
renderOpenPanels,
|
||||
renderRefundPanel,
|
||||
renderRetrievePanel,
|
||||
renderStatus,
|
||||
} from './services_view';
|
||||
import type { PortalBootstrapData, RefundState, ServiceStatus, VerificationFlowState } from './types';
|
||||
|
||||
function createBootstrap(overrides: Partial<PortalBootstrapData> = {}): PortalBootstrapData {
|
||||
return {
|
||||
authenticated: true,
|
||||
email: 'owner@example.com',
|
||||
public_site_url: 'https://pulserelay.pro',
|
||||
support_email: 'support@pulserelay.pro',
|
||||
commercial_api_base_url: 'https://license.pulserelay.pro',
|
||||
portal_path: '/portal',
|
||||
bootstrap_path: '/api/portal/bootstrap',
|
||||
magic_link_request_path: '/auth/magic-link',
|
||||
signup_path: '/signup',
|
||||
logout_path: '/auth/logout',
|
||||
account_api_base_path: '/api/accounts',
|
||||
portal_api_base_path: '/api/portal',
|
||||
accounts: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createFlowState(overrides: Partial<VerificationFlowState> = {}): VerificationFlowState {
|
||||
return {
|
||||
pendingEmail: '',
|
||||
requesting: false,
|
||||
confirming: false,
|
||||
step2Visible: false,
|
||||
status: {
|
||||
visible: false,
|
||||
message: '',
|
||||
error: false,
|
||||
},
|
||||
result: null,
|
||||
emailValue: '',
|
||||
codeValue: '',
|
||||
checkboxChecked: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createRefundState(overrides: Partial<RefundState> = {}): RefundState {
|
||||
return {
|
||||
emailValue: '',
|
||||
tokenValue: '',
|
||||
submitting: false,
|
||||
status: {
|
||||
visible: false,
|
||||
message: '',
|
||||
error: false,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('services view', function() {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('toggles the visible service panel by id', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="manage-service-panel" class="service-panel"></div>' +
|
||||
'<div id="retrieve-service-panel" class="service-panel"></div>' +
|
||||
'<div id="refund-service-panel" class="service-panel"></div>' +
|
||||
'<div id="data-service-panel" class="service-panel"></div>';
|
||||
|
||||
renderOpenPanels('retrieve-service-panel');
|
||||
|
||||
expect(document.getElementById('retrieve-service-panel')?.classList.contains('visible')).toBe(true);
|
||||
expect(document.getElementById('manage-service-panel')?.classList.contains('visible')).toBe(false);
|
||||
expect(document.getElementById('refund-service-panel')?.classList.contains('visible')).toBe(false);
|
||||
});
|
||||
|
||||
it('renders retrieve panel result state with invoice and token metadata', function() {
|
||||
document.body.innerHTML = '<div id="retrieve-service-root"></div>';
|
||||
|
||||
renderRetrievePanel(
|
||||
createFlowState({
|
||||
emailValue: 'buyer@example.com',
|
||||
codeValue: '123456',
|
||||
step2Visible: true,
|
||||
result: {
|
||||
invoice_url: 'https://license.pulserelay.pro/invoices/inv_123',
|
||||
token: 'pulse_key_123',
|
||||
tier: 'Relay',
|
||||
issued_at: '2026-03-26T10:00:00Z',
|
||||
expires_at: null,
|
||||
email: 'buyer@example.com',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
var copyButton = document.getElementById('retrieve-inline-copy') as HTMLButtonElement;
|
||||
var invoiceLink = document.getElementById('retrieve-inline-invoice') as HTMLAnchorElement;
|
||||
var tokenArea = document.getElementById('retrieve-inline-token') as HTMLTextAreaElement;
|
||||
|
||||
expect(copyButton.style.display).toBe('inline-block');
|
||||
expect(invoiceLink.href).toBe('https://license.pulserelay.pro/invoices/inv_123');
|
||||
expect(invoiceLink.style.display).toBe('inline-block');
|
||||
expect(tokenArea.value).toBe('pulse_key_123');
|
||||
expect(document.getElementById('retrieve-inline-email-value')?.textContent).toBe('buyer@example.com');
|
||||
expect(document.getElementById('retrieve-inline-result')?.style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('renders refund and delete panels from owned state', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="refund-service-root"></div>' +
|
||||
'<div id="data-delete-root"></div>';
|
||||
|
||||
renderRefundPanel(
|
||||
createRefundState({
|
||||
emailValue: 'owner@example.com',
|
||||
tokenValue: 'pulse_token',
|
||||
}),
|
||||
createBootstrap()
|
||||
);
|
||||
renderDeletePanel(
|
||||
createFlowState({
|
||||
emailValue: 'owner@example.com',
|
||||
codeValue: '654321',
|
||||
step2Visible: true,
|
||||
checkboxChecked: true,
|
||||
})
|
||||
);
|
||||
|
||||
var refundEmail = document.getElementById('refund-inline-email') as HTMLInputElement;
|
||||
var refundToken = document.getElementById('refund-inline-token') as HTMLInputElement;
|
||||
var deleteCheck = document.getElementById('data-delete-confirm-check') as HTMLInputElement;
|
||||
|
||||
expect(refundEmail.value).toBe('owner@example.com');
|
||||
expect(refundToken.value).toBe('pulse_token');
|
||||
expect(document.getElementById('refund-service-root')?.innerHTML).toContain('/refund.html?email=owner%40example.com');
|
||||
expect(deleteCheck.checked).toBe(true);
|
||||
expect(document.getElementById('data-delete-step2')?.style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('renders export panel and updates export result payload visibility', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="data-export-root"></div>' +
|
||||
'<div id="data-export-result" style="display:none"></div>' +
|
||||
'<textarea id="data-export-payload"></textarea>';
|
||||
|
||||
renderExportPanel(
|
||||
createFlowState({
|
||||
emailValue: 'owner@example.com',
|
||||
step2Visible: true,
|
||||
result: {
|
||||
accounts: 2,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
var payload = { accounts: 3, workspaces: 5 };
|
||||
renderExportResult(payload);
|
||||
|
||||
expect((document.getElementById('data-export-email') as HTMLInputElement).value).toBe('owner@example.com');
|
||||
expect(document.getElementById('data-export-step2')?.style.display).toBe('block');
|
||||
expect(document.getElementById('data-export-result')?.style.display).toBe('block');
|
||||
expect((document.getElementById('data-export-payload') as HTMLTextAreaElement).value).toBe(
|
||||
JSON.stringify(payload, null, 2)
|
||||
);
|
||||
});
|
||||
|
||||
it('renders manage panel and status classes from service state', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="manage-service-root"></div>' +
|
||||
'<div id="manage-inline-status" class="service-status"></div>';
|
||||
|
||||
renderManagePanel(
|
||||
createFlowState({
|
||||
emailValue: 'owner@example.com',
|
||||
codeValue: '123456',
|
||||
step2Visible: true,
|
||||
})
|
||||
);
|
||||
renderStatus('manage-inline-status', {
|
||||
visible: true,
|
||||
message: 'Code sent.',
|
||||
error: false,
|
||||
} satisfies ServiceStatus);
|
||||
|
||||
expect((document.getElementById('manage-inline-email') as HTMLInputElement).value).toBe('owner@example.com');
|
||||
expect((document.getElementById('manage-inline-code') as HTMLInputElement).value).toBe('123456');
|
||||
expect(document.getElementById('manage-inline-step2')?.style.display).toBe('block');
|
||||
expect(document.getElementById('manage-inline-status')?.className).toContain('success');
|
||||
expect(document.getElementById('manage-inline-status')?.textContent).toBe('Code sent.');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { PortalBootstrapData, PortalLoginState } from './types';
|
||||
import {
|
||||
renderAccountsHTML,
|
||||
renderAuthenticatedPortalHTML,
|
||||
renderHeaderHTML,
|
||||
renderSignedOutPortalHTML,
|
||||
type ShellViewContext,
|
||||
} from './shell_view';
|
||||
|
||||
function createBootstrap(overrides: Partial<PortalBootstrapData> = {}): PortalBootstrapData {
|
||||
return {
|
||||
authenticated: true,
|
||||
email: 'owner@example.com',
|
||||
public_site_url: 'https://pulserelay.pro',
|
||||
support_email: 'support@pulserelay.pro',
|
||||
commercial_api_base_url: 'https://license.pulserelay.pro',
|
||||
portal_path: '/portal',
|
||||
bootstrap_path: '/api/portal/bootstrap',
|
||||
magic_link_request_path: '/auth/magic-link',
|
||||
signup_path: '/signup',
|
||||
logout_path: '/auth/logout',
|
||||
account_api_base_path: '/api/accounts',
|
||||
portal_api_base_path: '/api/portal',
|
||||
accounts: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createLoginState(overrides: Partial<PortalLoginState> = {}): PortalLoginState {
|
||||
return {
|
||||
emailValue: '',
|
||||
sending: false,
|
||||
success: false,
|
||||
error: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createContext(overrides: Partial<ShellViewContext> = {}): ShellViewContext {
|
||||
return {
|
||||
bootstrap: createBootstrap(),
|
||||
loginState: createLoginState(),
|
||||
signupPath: '/signup',
|
||||
accountAPIBasePath: '/api/accounts',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('shell view', function() {
|
||||
it('renders authenticated header with account email and sign-out button', function() {
|
||||
var html = renderHeaderHTML(createContext());
|
||||
|
||||
expect(html).toContain('owner@example.com');
|
||||
expect(html).toContain('id="logout-btn"');
|
||||
expect(html).toContain('Sign out');
|
||||
});
|
||||
|
||||
it('renders signed-out header with create-account link', function() {
|
||||
var html = renderHeaderHTML(
|
||||
createContext({
|
||||
bootstrap: createBootstrap({ authenticated: false, email: '' }),
|
||||
})
|
||||
);
|
||||
|
||||
expect(html).toContain('href="/signup"');
|
||||
expect(html).toContain('Create account');
|
||||
expect(html).not.toContain('logout-btn" id="logout-btn"');
|
||||
});
|
||||
|
||||
it('renders empty accounts state with support contact', function() {
|
||||
var html = renderAccountsHTML(createContext());
|
||||
|
||||
expect(html).toContain('No workspaces found.');
|
||||
expect(html).toContain('mailto:support@pulserelay.pro');
|
||||
expect(html).toContain('support@pulserelay.pro');
|
||||
});
|
||||
|
||||
it('renders authenticated portal accounts, workspaces, and service entrypoints', function() {
|
||||
var html = renderAuthenticatedPortalHTML(
|
||||
createContext({
|
||||
bootstrap: createBootstrap({
|
||||
accounts: [
|
||||
{
|
||||
id: 'acct_1',
|
||||
name: 'Acme MSP',
|
||||
kind: 'msp',
|
||||
kind_label: 'MSP',
|
||||
role: 'owner',
|
||||
can_manage: true,
|
||||
has_billing: true,
|
||||
workspaces: [
|
||||
{
|
||||
id: 'ws_active',
|
||||
display_name: 'Alpha Workspace',
|
||||
state: 'active',
|
||||
healthy: true,
|
||||
created_at: '2026-03-26T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'ws_failed',
|
||||
display_name: 'Beta Workspace',
|
||||
state: 'failed',
|
||||
healthy: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
expect(html).toContain('<h1>Pulse Account</h1>');
|
||||
expect(html).toContain('id="accounts-root"');
|
||||
expect(html).toContain('Acme MSP');
|
||||
expect(html).toContain('Alpha Workspace');
|
||||
expect(html).toContain('/api/accounts/acct_1/tenants/ws_active/handoff');
|
||||
expect(html).toContain('data-action="workspace-manage"');
|
||||
expect(html).toContain('service-card-button');
|
||||
expect(html).toContain('id="open-retrieve-service"');
|
||||
expect(html).toContain('id="data-service-panel"');
|
||||
});
|
||||
|
||||
it('renders signed-out portal with error and success login states', function() {
|
||||
var errorHTML = renderSignedOutPortalHTML(
|
||||
createContext({
|
||||
bootstrap: createBootstrap({ authenticated: false, email: '' }),
|
||||
loginState: createLoginState({
|
||||
emailValue: 'buyer@example.com',
|
||||
error: 'Invalid email',
|
||||
}),
|
||||
})
|
||||
);
|
||||
var successHTML = renderSignedOutPortalHTML(
|
||||
createContext({
|
||||
bootstrap: createBootstrap({ authenticated: false, email: '' }),
|
||||
loginState: createLoginState({
|
||||
emailValue: 'buyer@example.com',
|
||||
success: true,
|
||||
sending: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
expect(errorHTML).toContain('value="buyer@example.com"');
|
||||
expect(errorHTML).toContain('Invalid email');
|
||||
expect(successHTML).toContain('Magic link sent.');
|
||||
expect(successHTML).toContain('data-portal-action="resend-magic-link"');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user