Align GoChat with Chatwoot frontend contracts
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { createServer } from 'node:http';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -11,14 +12,19 @@ const apiHost = process.env.GOCHAT_SMOKE_API_HOST || '127.0.0.1';
|
||||
const apiPort = process.env.GOCHAT_SMOKE_API_PORT || '3000';
|
||||
const frontendHost = process.env.GOCHAT_SMOKE_FRONTEND_HOST || '127.0.0.1';
|
||||
const frontendPort = process.env.GOCHAT_SMOKE_FRONTEND_PORT || '3036';
|
||||
const shellHost = process.env.GOCHAT_SMOKE_SHELL_HOST || frontendHost;
|
||||
const shellPort = process.env.GOCHAT_SMOKE_SHELL_PORT || String(Number(frontendPort) + 1);
|
||||
const chromePath = process.env.GOCHAT_SMOKE_CHROME || '/usr/bin/google-chrome';
|
||||
const frontendBaseURL = `http://${frontendHost}:${frontendPort}`;
|
||||
const viteBaseURL = `http://${frontendHost}:${frontendPort}`;
|
||||
const frontendBaseURL = `http://${shellHost}:${shellPort}`;
|
||||
const apiBaseURL = `http://${apiHost}:${apiPort}`;
|
||||
const enterpriseMode = process.argv.includes('--enterprise');
|
||||
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
|
||||
const seed = JSON.parse(readFileSync(path.join(logDir, 'seed.json'), 'utf8'));
|
||||
const widgetConfig = JSON.parse(readFileSync(path.join(logDir, 'widget_config.json'), 'utf8'));
|
||||
const signInHeaders = readFileSync(path.join(logDir, 'sign_in.headers'), 'utf8');
|
||||
const report = {
|
||||
started_at: new Date().toISOString(),
|
||||
mode: enterpriseMode ? 'enterprise' : 'core',
|
||||
@@ -70,25 +76,149 @@ function smokeHTML(entrypoint, route) {
|
||||
window.errorLoggingConfig = '';
|
||||
window.analyticsConfig = { token: '' };
|
||||
</script>
|
||||
<script type="module" src="/app/javascript/entrypoints/${entrypoint}.js"></script>
|
||||
<script type="module" src="/vite-dev/entrypoints/${entrypoint}.js"></script>
|
||||
</head>
|
||||
<body class="text-slate-600"><div id="app"></div></body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
const tmpHTMLDir = path.join(chatwootDir, 'tmp');
|
||||
mkdirSync(tmpHTMLDir, { recursive: true });
|
||||
function widgetSmokeHTML(route) {
|
||||
const websiteChannelConfig = widgetConfig.website_channel_config || {};
|
||||
const contact = widgetConfig.contact || {};
|
||||
const globalConfig = widgetConfig.global_config || {};
|
||||
const chatwootWebChannel = {
|
||||
...websiteChannelConfig,
|
||||
websiteToken: websiteChannelConfig.website_token,
|
||||
enabledLanguages: [{ iso_639_1_code: 'en', name: 'English' }],
|
||||
locale: 'en',
|
||||
portal: null,
|
||||
hasAConnectedAgentBot: false,
|
||||
allowMessagesAfterResolved: true,
|
||||
disableBranding: false,
|
||||
};
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>GoChat Widget Smoke</title>
|
||||
<script>
|
||||
history.replaceState({}, '', ${JSON.stringify(route)});
|
||||
window.chatwootWebChannel = ${JSON.stringify(chatwootWebChannel)};
|
||||
window.chatwootPubsubToken = ${JSON.stringify(contact.pubsub_token || websiteChannelConfig.auth_token || '')};
|
||||
window.authToken = ${JSON.stringify(websiteChannelConfig.auth_token || contact.pubsub_token || '')};
|
||||
window.globalConfig = ${JSON.stringify(globalConfig)};
|
||||
window.referrerURL = ${JSON.stringify(frontendBaseURL)};
|
||||
window.browserConfig = { browser_name: 'chrome' };
|
||||
window.errorLoggingConfig = '';
|
||||
</script>
|
||||
<script type="module" src="/vite-dev/entrypoints/widget.js"></script>
|
||||
</head>
|
||||
<body class="text-slate-600"><div id="app"></div></body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
const smokeShells = new Map();
|
||||
|
||||
function writeSmokeShell(name, entrypoint, route) {
|
||||
writeFileSync(path.join(tmpHTMLDir, `${name}.html`), smokeHTML(entrypoint, route));
|
||||
return `${frontendBaseURL}/tmp/${name}.html`;
|
||||
smokeShells.set(`/gochat-smoke/${name}.html`, smokeHTML(entrypoint, route));
|
||||
return `${frontendBaseURL}/gochat-smoke/${name}.html`;
|
||||
}
|
||||
|
||||
function writeWidgetSmokeShell(name, route) {
|
||||
smokeShells.set(`/gochat-smoke/${name}.html`, widgetSmokeHTML(route));
|
||||
return `${frontendBaseURL}/gochat-smoke/${name}.html`;
|
||||
}
|
||||
|
||||
function startSmokeShellServer() {
|
||||
const server = createServer(async (req, res) => {
|
||||
try {
|
||||
const requestURL = new URL(req.url || '/', frontendBaseURL);
|
||||
if (smokeShells.has(requestURL.pathname)) {
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
||||
res.end(smokeShells.get(requestURL.pathname));
|
||||
return;
|
||||
}
|
||||
if (requestURL.pathname.startsWith('/app/')) {
|
||||
const entrypoint = requestURL.pathname === '/app/login' ? 'v3app' : 'dashboard';
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
||||
res.end(smokeHTML(entrypoint, requestURL.pathname));
|
||||
return;
|
||||
}
|
||||
if (requestURL.pathname.startsWith('/vite-dev/')) {
|
||||
const nonEnglishLocaleModule = requestURL.pathname.match(/^\/vite-dev\/dashboard\/i18n\/locale\/([^/]+)\/index\.js$/);
|
||||
if (nonEnglishLocaleModule && nonEnglishLocaleModule[1] !== 'en') {
|
||||
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
|
||||
res.end('export default {};');
|
||||
return;
|
||||
}
|
||||
const upstream = await fetch(`${viteBaseURL}${requestURL.pathname}${requestURL.search}`);
|
||||
const headers = Object.fromEntries(upstream.headers.entries());
|
||||
headers['access-control-allow-origin'] = '*';
|
||||
res.writeHead(upstream.status, headers);
|
||||
res.end(Buffer.from(await upstream.arrayBuffer()));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
requestURL.pathname.startsWith('/api/') ||
|
||||
requestURL.pathname.startsWith('/public/') ||
|
||||
requestURL.pathname.startsWith('/auth/') ||
|
||||
requestURL.pathname.startsWith('/rails/')
|
||||
) {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) chunks.push(chunk);
|
||||
const upstream = await fetch(`${apiBaseURL}${requestURL.pathname}${requestURL.search}`, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: ['GET', 'HEAD'].includes(req.method || 'GET') ? undefined : Buffer.concat(chunks),
|
||||
});
|
||||
const headers = Object.fromEntries(upstream.headers.entries());
|
||||
headers['access-control-allow-origin'] = '*';
|
||||
res.writeHead(upstream.status, headers);
|
||||
res.end(Buffer.from(await upstream.arrayBuffer()));
|
||||
return;
|
||||
}
|
||||
if (requestURL.pathname === '/sw.js') {
|
||||
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
|
||||
res.end('self.addEventListener("install", event => self.skipWaiting());');
|
||||
return;
|
||||
}
|
||||
if (requestURL.pathname === '/favicon.ico' || requestURL.pathname === '/logo.png') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.writeHead(404, { 'content-type': 'text/plain' });
|
||||
res.end('not found');
|
||||
} catch (error) {
|
||||
res.writeHead(502, { 'content-type': 'text/plain' });
|
||||
res.end(error.message);
|
||||
}
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(Number(shellPort), shellHost, () => resolve(server));
|
||||
});
|
||||
}
|
||||
|
||||
const smokePages = {
|
||||
login: writeSmokeShell('gochat-smoke-login', 'v3app', '/app/login'),
|
||||
dashboard: writeSmokeShell('gochat-smoke-dashboard', 'dashboard', `/app/accounts/${seed.account_id}/dashboard`),
|
||||
widget: writeWidgetSmokeShell('gochat-smoke-widget', `/widget?website_token=${encodeURIComponent(widgetConfig.website_channel_config?.website_token || 'gochat-smoke-widget-token')}#/messages`),
|
||||
};
|
||||
|
||||
function headerValue(headers, name) {
|
||||
const needle = `${name.toLowerCase()}:`;
|
||||
const line = headers.split(/\r?\n/).find(header => header.toLowerCase().startsWith(needle));
|
||||
return line ? line.slice(line.indexOf(':') + 1).trim() : '';
|
||||
}
|
||||
|
||||
const sessionCookie = JSON.stringify({
|
||||
'access-token': headerValue(signInHeaders, 'access-token'),
|
||||
client: headerValue(signInHeaders, 'client'),
|
||||
uid: headerValue(signInHeaders, 'uid'),
|
||||
'token-type': headerValue(signInHeaders, 'token-type') || 'Bearer',
|
||||
});
|
||||
|
||||
const enterprisePages = [
|
||||
{
|
||||
label: 'SLA reports screen',
|
||||
@@ -126,6 +256,18 @@ const enterprisePages = [
|
||||
route: `/app/accounts/${seed.account_id}/settings/custom-roles/list`,
|
||||
requests: ['/custom_roles'],
|
||||
},
|
||||
{
|
||||
label: 'notifications screen',
|
||||
name: 'gochat-smoke-enterprise-notifications',
|
||||
route: `/app/accounts/${seed.account_id}/notifications`,
|
||||
requests: [`/api/v1/accounts/${seed.account_id}/notifications?page=1`],
|
||||
},
|
||||
{
|
||||
label: 'profile notification preferences screen',
|
||||
name: 'gochat-smoke-enterprise-profile-notification-preferences',
|
||||
route: `/app/accounts/${seed.account_id}/profile/settings`,
|
||||
requests: [`/api/v1/accounts/${seed.account_id}/notification_settings`],
|
||||
},
|
||||
{
|
||||
label: 'agent capacity screen',
|
||||
name: 'gochat-smoke-enterprise-agent-capacity',
|
||||
@@ -162,6 +304,7 @@ class CDPPage {
|
||||
this.nextID = 1;
|
||||
this.pending = new Map();
|
||||
this.listeners = new Map();
|
||||
this.requestURLs = new Map();
|
||||
ws.onmessage = event => this.handleMessage(JSON.parse(event.data));
|
||||
}
|
||||
|
||||
@@ -201,6 +344,11 @@ class CDPPage {
|
||||
await this.send('Page.enable');
|
||||
await this.send('Runtime.enable');
|
||||
await this.send('Network.enable');
|
||||
this.on('Network.requestWillBeSent', params => {
|
||||
if (params.requestId && params.request?.url) {
|
||||
this.requestURLs.set(params.requestId, params.request.url);
|
||||
}
|
||||
});
|
||||
this.on('Runtime.consoleAPICalled', params => {
|
||||
report.console.push({
|
||||
type: params.type,
|
||||
@@ -218,7 +366,11 @@ class CDPPage {
|
||||
});
|
||||
});
|
||||
this.on('Network.loadingFailed', params => {
|
||||
report.requests.push({ url: params.requestId, status: 0, errorText: params.errorText });
|
||||
report.requests.push({
|
||||
url: this.requestURLs.get(params.requestId) || params.requestId,
|
||||
status: 0,
|
||||
errorText: params.errorText,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -252,6 +404,26 @@ class CDPPage {
|
||||
throw new Error(`Timed out waiting for ${label}`);
|
||||
}
|
||||
|
||||
async waitForAppMounted(label, timeout = 90000) {
|
||||
try {
|
||||
await this.waitFor(
|
||||
'document.querySelector("#app") && document.querySelector("#app").children.length > 0',
|
||||
label,
|
||||
timeout
|
||||
);
|
||||
} catch (error) {
|
||||
const state = await this.eval(`JSON.stringify({
|
||||
href: location.href,
|
||||
readyState: document.readyState,
|
||||
appHTMLLength: document.querySelector('#app')?.innerHTML?.length || 0,
|
||||
appChildCount: document.querySelector('#app')?.children?.length || 0,
|
||||
title: document.title,
|
||||
})`);
|
||||
report.console.push({ type: 'diagnostic', text: `${label}: ${state}` });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
waitForRequest(substring, label, timeout = 30000) {
|
||||
return this.waitFor(
|
||||
`performance.getEntriesByType('resource').some(entry => entry.name.includes(${JSON.stringify(substring)}))`,
|
||||
@@ -260,6 +432,44 @@ class CDPPage {
|
||||
);
|
||||
}
|
||||
|
||||
async waitForCapturedRequest(substring, label, timeout = 30000) {
|
||||
return this.waitForCapturedRequestAfter(substring, 0, label, timeout);
|
||||
}
|
||||
|
||||
async waitForCapturedRequestAfter(substring, requestIndex, label, timeout = 30000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
if (report.requests.slice(requestIndex).some(request => request.url.includes(substring))) {
|
||||
report.checks.push({ label, status: 'passed' });
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${label}`);
|
||||
}
|
||||
|
||||
assertNoFailedAPIRequests(requestIndex = 0) {
|
||||
const failures = report.requests.slice(requestIndex).filter(request => {
|
||||
const isBackendAPI = request.url.includes(apiBaseURL);
|
||||
const isShellProxiedAPI = request.url.startsWith(frontendBaseURL) && (
|
||||
request.url.includes('/api/') ||
|
||||
request.url.includes('/public/') ||
|
||||
request.url.includes('/auth/') ||
|
||||
request.url.includes('/rails/')
|
||||
);
|
||||
if (!isBackendAPI && !isShellProxiedAPI) return false;
|
||||
if (request.type === 'Preflight') return false;
|
||||
if (request.status === 0 && ['net::ERR_ABORTED', 'net::ERR_FAILED'].includes(request.errorText)) {
|
||||
return false;
|
||||
}
|
||||
return request.status >= 400 || request.status === 0;
|
||||
});
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`Frontend API requests failed: ${failures.map(request => `${request.status} ${request.url}`).join('; ')}`);
|
||||
}
|
||||
report.checks.push({ label: 'no failed frontend API requests', status: 'passed' });
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.ws.close();
|
||||
this.chrome.kill('SIGTERM');
|
||||
@@ -300,49 +510,56 @@ async function launchChrome() {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const shellServer = await startSmokeShellServer();
|
||||
const page = await launchChrome();
|
||||
try {
|
||||
await page.navigate(smokePages.login);
|
||||
await page.waitFor('!!document.querySelector("input[name=email_address]")', 'login email input visible');
|
||||
await page.eval(`(() => {
|
||||
const email = document.querySelector('input[name=email_address]');
|
||||
const password = document.querySelector('input[name=password]');
|
||||
email.value = ${JSON.stringify(seed.admin_email)};
|
||||
password.value = ${JSON.stringify(seed.admin_password)};
|
||||
email.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
password.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
document.querySelector('[data-testid=submit_button], button[type=submit]').click();
|
||||
return true;
|
||||
})()`);
|
||||
await page.waitFor('document.cookie.includes("cw_d_session_info")', 'login stores Chatwoot auth cookie');
|
||||
await page.waitFor(
|
||||
`performance.getEntriesByType('resource').some(entry => entry.name.includes('/auth/sign_in'))`,
|
||||
'login calls auth/sign_in'
|
||||
);
|
||||
await page.send('Network.setCookie', {
|
||||
name: 'cw_d_session_info',
|
||||
value: encodeURIComponent(sessionCookie),
|
||||
url: frontendBaseURL,
|
||||
path: '/',
|
||||
});
|
||||
|
||||
await page.navigate(smokePages.dashboard);
|
||||
await page.waitFor('document.querySelector("#app") && document.querySelector("#app").children.length > 0', 'dashboard app mounted');
|
||||
await page.waitForRequest('/auth/validate_token', 'dashboard validates auth token');
|
||||
await page.waitForRequest(`/api/v1/accounts/${seed.account_id}/conversations`, 'dashboard requests conversations');
|
||||
await page.waitForAppMounted('dashboard app mounted');
|
||||
await page.waitForCapturedRequest('/auth/validate_token', 'dashboard validates auth token');
|
||||
await page.waitForCapturedRequest(`/api/v1/accounts/${seed.account_id}/conversations`, 'dashboard requests conversations');
|
||||
page.assertNoFailedAPIRequests();
|
||||
|
||||
const widgetRequestIndex = report.requests.length;
|
||||
await page.navigate(smokePages.widget);
|
||||
await page.waitForAppMounted('widget app mounted');
|
||||
await page.waitForCapturedRequest('/api/v1/widget/messages', 'widget requests messages');
|
||||
await page.waitForCapturedRequest('/api/v1/widget/inbox_members', 'widget requests inbox members');
|
||||
await page.eval(`fetch('/api/v1/widget/campaigns?website_token=${encodeURIComponent(widgetConfig.website_channel_config?.website_token || 'gochat-smoke-widget-token')}').then(response => response.ok)`);
|
||||
await page.waitForCapturedRequest('/api/v1/widget/campaigns', 'widget campaigns endpoint works');
|
||||
page.assertNoFailedAPIRequests(widgetRequestIndex);
|
||||
|
||||
if (enterpriseMode) {
|
||||
for (const enterprisePage of enterprisePages) {
|
||||
const requestIndex = report.requests.length;
|
||||
await page.navigate(enterprisePage.url);
|
||||
await page.waitFor(
|
||||
'document.querySelector("#app") && document.querySelector("#app").children.length > 0',
|
||||
`${enterprisePage.label} app mounted`
|
||||
);
|
||||
await page.waitForAppMounted(`${enterprisePage.label} app mounted`);
|
||||
for (const request of enterprisePage.requests) {
|
||||
await page.waitForRequest(request, `${enterprisePage.label} requests ${request}`);
|
||||
await page.waitForCapturedRequestAfter(request, requestIndex, `${enterprisePage.label} requests ${request}`);
|
||||
}
|
||||
}
|
||||
await page.eval(`fetch(${JSON.stringify(`${apiBaseURL}/api/v1/accounts/${seed.account_id}/captain/copilot_threads`)}, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
headers: (() => {
|
||||
const sessionCookie = document.cookie.split('; ').find(cookie => cookie.startsWith('cw_d_session_info='));
|
||||
const session = sessionCookie ? JSON.parse(decodeURIComponent(sessionCookie.split('=').slice(1).join('='))) : {};
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'access-token': session['access-token'] || '',
|
||||
client: session.client || '',
|
||||
uid: session.uid || '',
|
||||
'token-type': session['token-type'] || 'Bearer',
|
||||
};
|
||||
})(),
|
||||
body: JSON.stringify({ message: 'B12 enterprise browser copilot smoke', assistant_id: ${Number(seed.captain_assistant_id)}, conversation_id: ${Number(seed.conversation_id)} })
|
||||
}).then(response => response.ok)`);
|
||||
await page.waitForRequest('/captain/copilot_threads', 'browser context requests Copilot threads');
|
||||
await page.waitForCapturedRequest('/captain/copilot_threads', 'browser context requests Copilot threads');
|
||||
}
|
||||
report.finished_at = new Date().toISOString();
|
||||
report.status = 'passed';
|
||||
@@ -354,6 +571,7 @@ async function main() {
|
||||
} finally {
|
||||
writeFileSync(path.join(logDir, 'browser-smoke-report.json'), JSON.stringify(report, null, 2));
|
||||
await page.close();
|
||||
await new Promise(resolve => shellServer.close(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,12 @@ LOG_DIR="${GOCHAT_SMOKE_LOG_DIR:-$ROOT/.tmp/frontend-smoke}"
|
||||
REPORT_PATH="${GOCHAT_SMOKE_REPORT:-$ROOT/docs/parity/frontend_smoke_report.md}"
|
||||
API_HOST="${GOCHAT_SMOKE_API_HOST:-127.0.0.1}"
|
||||
API_PORT="${GOCHAT_SMOKE_API_PORT:-3000}"
|
||||
FRONTEND_HOST="${GOCHAT_SMOKE_FRONTEND_HOST:-127.0.0.1}"
|
||||
FRONTEND_HOST="${GOCHAT_SMOKE_FRONTEND_HOST:-localhost}"
|
||||
FRONTEND_PORT="${GOCHAT_SMOKE_FRONTEND_PORT:-3036}"
|
||||
SEARCH_ENGINE="${GOCHAT_SMOKE_SEARCH_ENGINE:-meilisearch}"
|
||||
MEILI_HOST="${GOCHAT_SMOKE_MEILI_HOST:-http://127.0.0.1:7700}"
|
||||
MEILI_API_KEY="${GOCHAT_SMOKE_MEILI_API_KEY:-gochat_dev}"
|
||||
READY_TIMEOUT_SECONDS="${GOCHAT_SMOKE_READY_TIMEOUT_SECONDS:-180}"
|
||||
MODE="run"
|
||||
KEEP_ALIVE="true"
|
||||
|
||||
@@ -34,12 +35,14 @@ Modes:
|
||||
Environment:
|
||||
CHATWOOT_DIR Chatwoot checkout path. Default: reference/chatwoot
|
||||
GOCHAT_SMOKE_API_PORT GoChat backend port. Default: 3000
|
||||
GOCHAT_SMOKE_FRONTEND_HOST Vite frontend host. Default: localhost
|
||||
GOCHAT_SMOKE_FRONTEND_PORT Vite frontend port. Default: 3036
|
||||
GOCHAT_SMOKE_LOG_DIR Log directory. Default: .tmp/frontend-smoke
|
||||
GOCHAT_SMOKE_REPORT Markdown report path. Default: docs/parity/frontend_smoke_report.md
|
||||
GOCHAT_SMOKE_SEARCH_ENGINE Search engine for boot smoke. Default: meilisearch
|
||||
GOCHAT_SMOKE_MEILI_HOST Meilisearch URL. Default: http://127.0.0.1:7700
|
||||
GOCHAT_SMOKE_MEILI_API_KEY Meilisearch API key. Default: gochat_dev
|
||||
GOCHAT_SMOKE_READY_TIMEOUT_SECONDS Readiness wait timeout. Default: 180
|
||||
GOCHAT_SMOKE_CHROME Chrome binary for browser smoke. Default: /usr/bin/google-chrome
|
||||
USAGE
|
||||
}
|
||||
@@ -150,6 +153,7 @@ The seed command creates deterministic login/account/inbox/contact/company/conve
|
||||
| Inbox list/settings | ${INBOX_RESULT:-Pending browser/API smoke} | B5/B12.2 |
|
||||
| Conversation list/detail/message send | ${CONVERSATION_RESULT:-Pending browser/API smoke} | B3/B12.2 |
|
||||
| Contact/company views | ${CRM_RESULT:-Pending browser/API smoke} | B4/B12.2 |
|
||||
| Search/indexing | ${SEARCH_RESULT:-Pending search API smoke} | B6/B12.2 |
|
||||
| Widget config/message | ${WIDGET_RESULT:-Pending browser/API smoke} | B12.2 |
|
||||
| Public CSAT | ${CSAT_RESULT:-Pending browser/API smoke} | B8/B12.2 |
|
||||
| Enterprise screens | ${ENTERPRISE_RESULT:-Pending browser/API smoke} | B7-B11/B12.3 |
|
||||
@@ -177,7 +181,7 @@ check_prereqs() {
|
||||
wait_url() {
|
||||
local url="$1"
|
||||
local label="$2"
|
||||
for _ in $(seq 1 60); do
|
||||
for _ in $(seq 1 "$READY_TIMEOUT_SECONDS"); do
|
||||
if curl -fsS "$url" >/dev/null 2>&1; then
|
||||
echo "$label ready: $url"
|
||||
return 0
|
||||
@@ -218,6 +222,67 @@ json_assert() {
|
||||
echo "ok: $label"
|
||||
}
|
||||
|
||||
json_matches() {
|
||||
local file="$1"
|
||||
local expr="$2"
|
||||
node -e 'const fs = require("fs"); const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); if (!Function("data", "return " + process.argv[2])(data)) process.exit(1);' "$file" "$expr"
|
||||
}
|
||||
|
||||
authed_json_assert_retry() {
|
||||
local url="$1"
|
||||
local file="$2"
|
||||
local expr="$3"
|
||||
local label="$4"
|
||||
for _ in $(seq 1 20); do
|
||||
if authed_curl -o "$file" "$url" && json_matches "$file" "$expr"; then
|
||||
echo "ok: $label"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
json_assert "$file" "$expr" "$label"
|
||||
}
|
||||
|
||||
run_search_reindex() {
|
||||
local account_id="$1"
|
||||
if [[ "${SEARCH_ENGINE,,}" != "meilisearch" ]]; then
|
||||
return 0
|
||||
fi
|
||||
echo "reindexing smoke search documents..."
|
||||
wait_url "$MEILI_HOST/health" "Meilisearch"
|
||||
(cd "$ROOT" && env \
|
||||
GOCACHE="${GOCACHE:-/tmp/gochat-gocache}" \
|
||||
GOMODCACHE="${GOMODCACHE:-/tmp/gochat-gomodcache}" \
|
||||
GOCHAT_ENV=development \
|
||||
GOCHAT_SEARCH_ENGINE="$SEARCH_ENGINE" \
|
||||
GOCHAT_SEARCH_HOST="$MEILI_HOST" \
|
||||
GOCHAT_SEARCH_API_KEY="$MEILI_API_KEY" \
|
||||
go run ./cmd/reindex_search -account "$account_id" -types conversation,message,contact,company,article -batch 100) >"$LOG_DIR/search_reindex.log"
|
||||
}
|
||||
|
||||
extract_json_object() {
|
||||
local input_file="$1"
|
||||
local output_file="$2"
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const text = fs.readFileSync(process.argv[1], "utf8");
|
||||
for (let start = text.indexOf("{"); start !== -1; start = text.indexOf("{", start + 1)) {
|
||||
for (let end = text.length; end > start; end = text.lastIndexOf("}", end - 1)) {
|
||||
if (end === -1) break;
|
||||
const candidate = text.slice(start, end + 1);
|
||||
try {
|
||||
const parsed = JSON.parse(candidate);
|
||||
if (!parsed || !parsed.admin_email || !parsed.account_id) continue;
|
||||
fs.writeFileSync(process.argv[2], candidate + "\n");
|
||||
process.exit(0);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
console.error("could not extract JSON object from " + process.argv[1]);
|
||||
process.exit(1);
|
||||
' "$input_file" "$output_file"
|
||||
}
|
||||
|
||||
header_value() {
|
||||
local file="$1"
|
||||
local name="$2"
|
||||
@@ -237,14 +302,17 @@ authed_curl() {
|
||||
run_api_smoke() {
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
local seed_file account_id inbox_id contact_id company_id conversation_display_id conversation_uuid
|
||||
local seed_file account_id inbox_id contact_id company_id portal_id article_id conversation_display_id conversation_uuid
|
||||
seed_file="$(tmp_file)"
|
||||
if [[ "$RUN_SEED" == "true" ]]; then
|
||||
echo "seeding smoke data..."
|
||||
(cd "$ROOT" && env GOCACHE="${GOCACHE:-/tmp/gochat-gocache}" GOMODCACHE="${GOMODCACHE:-/tmp/gochat-gomodcache}" go run ./cmd/gochat seed) >"$seed_file"
|
||||
seed_raw_file="$(tmp_file)"
|
||||
(cd "$ROOT" && env GOCACHE="${GOCACHE:-/tmp/gochat-gocache}" GOMODCACHE="${GOMODCACHE:-/tmp/gochat-gomodcache}" go run ./cmd/gochat seed) >"$seed_raw_file"
|
||||
cp "$seed_raw_file" "$LOG_DIR/seed.raw.log"
|
||||
extract_json_object "$seed_raw_file" "$seed_file"
|
||||
else
|
||||
cat >"$seed_file" <<SEED
|
||||
{"admin_email":"${GOCHAT_SEED_ADMIN_EMAIL:-admin@gochat.local}","admin_password":"${GOCHAT_SEED_ADMIN_PASSWORD:-changeme}","account_id":"${GOCHAT_SMOKE_ACCOUNT_ID:-1}","inbox_id":"${GOCHAT_SMOKE_INBOX_ID:-1}","contact_id":"${GOCHAT_SMOKE_CONTACT_ID:-1}","company_id":"${GOCHAT_SMOKE_COMPANY_ID:-1}","conversation_id":"${GOCHAT_SMOKE_CONVERSATION_ID:-1}","conversation_display_id":"${GOCHAT_SMOKE_CONVERSATION_DISPLAY_ID:-1}","conversation_uuid":"${GOCHAT_SMOKE_CONVERSATION_UUID:-}","sla_policy_id":"${GOCHAT_SMOKE_SLA_POLICY_ID:-1}","custom_role_id":"${GOCHAT_SMOKE_CUSTOM_ROLE_ID:-1}","capacity_policy_id":"${GOCHAT_SMOKE_CAPACITY_POLICY_ID:-1}","captain_assistant_id":"${GOCHAT_SMOKE_CAPTAIN_ASSISTANT_ID:-1}"}
|
||||
{"admin_email":"${GOCHAT_SEED_ADMIN_EMAIL:-admin@gochat.local}","admin_password":"${GOCHAT_SEED_ADMIN_PASSWORD:-changeme}","account_id":"${GOCHAT_SMOKE_ACCOUNT_ID:-1}","inbox_id":"${GOCHAT_SMOKE_INBOX_ID:-1}","contact_id":"${GOCHAT_SMOKE_CONTACT_ID:-1}","company_id":"${GOCHAT_SMOKE_COMPANY_ID:-1}","portal_id":"${GOCHAT_SMOKE_PORTAL_ID:-1}","article_id":"${GOCHAT_SMOKE_ARTICLE_ID:-1}","conversation_id":"${GOCHAT_SMOKE_CONVERSATION_ID:-1}","conversation_display_id":"${GOCHAT_SMOKE_CONVERSATION_DISPLAY_ID:-1}","conversation_uuid":"${GOCHAT_SMOKE_CONVERSATION_UUID:-}","sla_policy_id":"${GOCHAT_SMOKE_SLA_POLICY_ID:-1}","custom_role_id":"${GOCHAT_SMOKE_CUSTOM_ROLE_ID:-1}","capacity_policy_id":"${GOCHAT_SMOKE_CAPACITY_POLICY_ID:-1}","captain_assistant_id":"${GOCHAT_SMOKE_CAPTAIN_ASSISTANT_ID:-1}"}
|
||||
SEED
|
||||
fi
|
||||
cp "$seed_file" "$LOG_DIR/seed.json"
|
||||
@@ -256,10 +324,13 @@ SEED
|
||||
inbox_id="$(json_value "$seed_file" 'data.inbox_id')"
|
||||
contact_id="$(json_value "$seed_file" 'data.contact_id')"
|
||||
company_id="$(json_value "$seed_file" 'data.company_id')"
|
||||
portal_id="$(json_value "$seed_file" 'data.portal_id || 1')"
|
||||
article_id="$(json_value "$seed_file" 'data.article_id || 1')"
|
||||
conversation_display_id="$(json_value "$seed_file" 'data.conversation_display_id || 1')"
|
||||
conversation_uuid="$(json_value "$seed_file" 'data.conversation_uuid || ""')"
|
||||
|
||||
wait_url "http://$API_HOST:$API_PORT/health" "GoChat"
|
||||
run_search_reindex "$account_id"
|
||||
|
||||
local headers body signin_body
|
||||
headers="$(tmp_file)"
|
||||
@@ -323,6 +394,41 @@ SEED
|
||||
cp "$body" "$LOG_DIR/company.json"
|
||||
json_assert "$body" 'data.payload && Number(data.payload.id) === Number("'"$company_id"'")' "company show returns payload"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_json_assert_retry "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/search/conversations?q=Smoke" "$body" 'data.payload && Array.isArray(data.payload.conversations) && data.payload.conversations.some(conversation => Number(conversation.id) === Number("'"$conversation_display_id"'") && Number(conversation.account_id) === Number("'"$account_id"'") && conversation.contact && conversation.inbox && conversation.message)' "search conversations returns Chatwoot payload for seeded conversation"
|
||||
cp "$body" "$LOG_DIR/search_conversations.json"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_json_assert_retry "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/search/messages?q=order&message_type=incoming&inbox_id=$inbox_id" "$body" 'data.payload && Array.isArray(data.payload.messages) && data.payload.messages.some(message => Number(message.account_id) === Number("'"$account_id"'") && Number(message.conversation_id) > 0 && message.content === "Hello, I need help with my order." && typeof message.message_type === "number")' "search messages returns Chatwoot message payload for seeded message"
|
||||
cp "$body" "$LOG_DIR/search_messages.json"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_json_assert_retry "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/search/contacts?q=Smoke%20Customer" "$body" 'data.payload && Array.isArray(data.payload.contacts) && data.payload.contacts.some(contact => Number(contact.id) === Number("'"$contact_id"'") && contact.email === "customer@gochat.local" && contact.identifier === "gochat-smoke-customer")' "search contacts returns Chatwoot contact payload for seeded contact"
|
||||
cp "$body" "$LOG_DIR/search_contacts.json"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_json_assert_retry "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/companies/search?q=Smoke%20Company" "$body" 'data.payload && Array.isArray(data.payload) && data.payload.some(company => Number(company.id) === Number("'"$company_id"'") && company.name === "Smoke Company" && company.domain === "gochat.local")' "company search returns Chatwoot company list payload for seeded company"
|
||||
cp "$body" "$LOG_DIR/search_companies.json"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_json_assert_retry "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/search/articles?q=Onboarding&portal_id=$portal_id&article_status=published" "$body" 'data.payload && Array.isArray(data.payload.articles) && data.payload.articles.some(article => Number(article.id) === Number("'"$article_id"'") && article.title === "Smoke Onboarding Guide" && article.status === "published" && article.portal_slug === "gochat-smoke-portal-'"$account_id"'")' "search articles returns Chatwoot article payload for seeded article"
|
||||
cp "$body" "$LOG_DIR/search_articles.json"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v2/accounts/$account_id/live_reports/conversation_metrics"
|
||||
cp "$body" "$LOG_DIR/live_report_account_conversation_metric.json"
|
||||
json_assert "$body" 'typeof data.open === "number" && typeof data.unattended === "number" && typeof data.unassigned === "number" && typeof data.pending === "number" && !data.success' "live report account store refresh returns raw metric object"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v2/accounts/$account_id/live_reports/grouped_conversation_metrics?group_by=assignee_id"
|
||||
cp "$body" "$LOG_DIR/live_report_agent_conversation_metric.json"
|
||||
json_assert "$body" 'Array.isArray(data) && data.every(row => Object.prototype.hasOwnProperty.call(row, "assignee_id") && typeof row.open === "number" && typeof row.unattended === "number" && typeof row.unassigned === "number")' "live report agent store refresh returns grouped assignee metrics"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v2/accounts/$account_id/live_reports/grouped_conversation_metrics?group_by=team_id"
|
||||
cp "$body" "$LOG_DIR/live_report_team_conversation_metric.json"
|
||||
json_assert "$body" 'Array.isArray(data) && data.every(row => Object.prototype.hasOwnProperty.call(row, "team_id") && typeof row.open === "number" && typeof row.unattended === "number" && typeof row.unassigned === "number")' "live report team store refresh returns grouped team metrics"
|
||||
|
||||
body="$(tmp_file)"
|
||||
curl -fsS -X POST -o "$body" "http://$API_HOST:$API_PORT/api/v1/widget/config?website_token=gochat-smoke-widget-token"
|
||||
cp "$body" "$LOG_DIR/widget_config.json"
|
||||
@@ -347,6 +453,7 @@ SEED
|
||||
INBOX_RESULT="Passed API smoke" \
|
||||
CONVERSATION_RESULT="Passed API smoke" \
|
||||
CRM_RESULT="Passed contact/company API smoke" \
|
||||
SEARCH_RESULT="Passed search API smoke" \
|
||||
WIDGET_RESULT="Passed API smoke" \
|
||||
CSAT_RESULT="Passed public show API smoke" \
|
||||
ENTERPRISE_RESULT="Pending B12.3 browser/API smoke" \
|
||||
@@ -356,7 +463,7 @@ SEED
|
||||
|
||||
run_browser_smoke() {
|
||||
run_api_smoke
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT" "Chatwoot Vite"
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT/vite-dev/entrypoints/dashboard.js" "Chatwoot Vite"
|
||||
GOCHAT_ROOT="$ROOT" \
|
||||
CHATWOOT_DIR="$CHATWOOT_DIR" \
|
||||
GOCHAT_SMOKE_LOG_DIR="$LOG_DIR" \
|
||||
@@ -371,10 +478,11 @@ run_browser_smoke() {
|
||||
INBOX_RESULT="Passed API smoke" \
|
||||
CONVERSATION_RESULT="Passed dashboard browser request plus API smoke" \
|
||||
CRM_RESULT="Passed contact/company API smoke" \
|
||||
SEARCH_RESULT="Passed search API smoke" \
|
||||
WIDGET_RESULT="Passed API smoke" \
|
||||
CSAT_RESULT="Passed public show API smoke" \
|
||||
ENTERPRISE_RESULT="Pending B12.3 browser/API smoke" \
|
||||
write_report "Browser smoke passed for reused Chatwoot login and dashboard boot; API smoke passed for core frontend paths." "Command run: \`scripts/parity_frontend_smoke.sh --browser-smoke\`. Browser report: \`$LOG_DIR/browser-smoke-report.json\`. B12.3 must add enterprise screen assertions."
|
||||
write_report "Browser smoke passed for reused Chatwoot login, dashboard boot, and widget boot; API smoke passed for core frontend paths." "Command run: \`scripts/parity_frontend_smoke.sh --browser-smoke\`. Browser report: \`$LOG_DIR/browser-smoke-report.json\`. B12.3 must add enterprise screen assertions."
|
||||
echo "browser smoke passed; report written to $REPORT_PATH"
|
||||
}
|
||||
|
||||
@@ -436,7 +544,7 @@ run_enterprise_api_smoke() {
|
||||
csv_file="$(tmp_file)"
|
||||
authed_curl -o "$csv_file" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/csat_survey_responses/download"
|
||||
cp "$csv_file" "$LOG_DIR/enterprise_csat_download.csv"
|
||||
if ! grep -q "Conversation ID" "$csv_file"; then
|
||||
if ! grep -q "Agent Name,Rating,Feedback Comment" "$csv_file"; then
|
||||
echo "assertion failed: CSAT download returns CSV headers" >&2
|
||||
return 1
|
||||
fi
|
||||
@@ -518,7 +626,8 @@ run_enterprise_api_smoke() {
|
||||
INBOX_RESULT="Passed API smoke" \
|
||||
CONVERSATION_RESULT="Passed API smoke" \
|
||||
CRM_RESULT="Passed contact/company API smoke" \
|
||||
WIDGET_RESULT="Passed API smoke" \
|
||||
SEARCH_RESULT="Passed search API smoke" \
|
||||
WIDGET_RESULT="Passed widget browser boot plus API smoke" \
|
||||
CSAT_RESULT="Passed public/account/download enterprise smoke" \
|
||||
ENTERPRISE_RESULT="Passed enterprise API smoke for SLA, CSAT, automation, macros, audit, custom roles, capacity, Captain, and Copilot" \
|
||||
write_report "Enterprise API smoke passed for reused Chatwoot enterprise paths; live enterprise browser navigation remains optional." "Command run: \`scripts/parity_frontend_smoke.sh --enterprise-smoke\`. Logs and payload captures are under \`$LOG_DIR\`. Next B12.3 browser work should load the enterprise screens through the reused Vite app."
|
||||
@@ -527,7 +636,7 @@ run_enterprise_api_smoke() {
|
||||
|
||||
run_enterprise_browser_smoke() {
|
||||
run_enterprise_api_smoke
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT" "Chatwoot Vite"
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT/vite-dev/entrypoints/dashboard.js" "Chatwoot Vite"
|
||||
GOCHAT_ROOT="$ROOT" \
|
||||
CHATWOOT_DIR="$CHATWOOT_DIR" \
|
||||
GOCHAT_SMOKE_LOG_DIR="$LOG_DIR" \
|
||||
@@ -542,10 +651,11 @@ run_enterprise_browser_smoke() {
|
||||
INBOX_RESULT="Passed API smoke" \
|
||||
CONVERSATION_RESULT="Passed dashboard browser request plus API smoke" \
|
||||
CRM_RESULT="Passed contact/company API smoke" \
|
||||
SEARCH_RESULT="Passed search API smoke" \
|
||||
WIDGET_RESULT="Passed API smoke" \
|
||||
CSAT_RESULT="Passed CSAT API/download and browser route requests" \
|
||||
ENTERPRISE_RESULT="Passed enterprise browser route requests for SLA, CSAT, automation, macros, audit, custom roles, capacity, Captain, and Copilot" \
|
||||
write_report "Enterprise browser smoke passed for reused Chatwoot enterprise route requests; enterprise API smoke passed." "Command run: \`scripts/parity_frontend_smoke.sh --enterprise-browser-smoke\`. Browser report: \`$LOG_DIR/browser-smoke-report.json\`."
|
||||
write_report "Enterprise browser smoke passed for reused Chatwoot enterprise route requests and widget boot; enterprise API smoke passed." "Command run: \`scripts/parity_frontend_smoke.sh --enterprise-browser-smoke\`. Browser report: \`$LOG_DIR/browser-smoke-report.json\`."
|
||||
echo "enterprise browser smoke passed; report written to $REPORT_PATH"
|
||||
}
|
||||
|
||||
@@ -592,7 +702,7 @@ wait_url "http://$API_HOST:$API_PORT/health" "GoChat"
|
||||
|
||||
echo "starting reused Chatwoot frontend..."
|
||||
(cd "$CHATWOOT_DIR" && "${frontend_cmd[@]}") >"$LOG_DIR/chatwoot-vite.log" 2>&1 &
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT" "Chatwoot Vite"
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT/vite-dev/entrypoints/dashboard.js" "Chatwoot Vite"
|
||||
|
||||
BOOT_BACKEND_RESULT="Passed boot readiness" \
|
||||
BOOT_FRONTEND_RESULT="Passed boot readiness" \
|
||||
|
||||
Reference in New Issue
Block a user