feat: complete current GoChat updates
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import { spawn } from "node:child_process";
|
||||
import {
|
||||
closeSync,
|
||||
mkdirSync,
|
||||
@@ -7,70 +7,110 @@ import {
|
||||
openSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { createServer } from 'node:http';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
} from "node:fs";
|
||||
import { createServer } from "node:http";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const root = process.env.GOCHAT_ROOT || process.cwd();
|
||||
const chatwootDir = process.env.CHATWOOT_DIR || path.join(root, '..', 'docs', 'chatwoot');
|
||||
const logDir = process.env.GOCHAT_SMOKE_LOG_DIR || path.join(root, '.tmp/frontend-smoke');
|
||||
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 frontendDir =
|
||||
process.env.FRONTEND_DIR ||
|
||||
process.env.CHATWOOT_DIR ||
|
||||
path.join(root, "..", "frontend");
|
||||
const logDir =
|
||||
process.env.GOCHAT_SMOKE_LOG_DIR || path.join(root, ".tmp/frontend-smoke");
|
||||
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 shellPort =
|
||||
process.env.GOCHAT_SMOKE_SHELL_PORT || String(Number(frontendPort) + 1);
|
||||
const chromePath = process.env.GOCHAT_SMOKE_CHROME || "/usr/bin/google-chrome";
|
||||
const viteBaseURL = `http://${frontendHost}:${frontendPort}`;
|
||||
const frontendBaseURL = `http://${shellHost}:${shellPort}`;
|
||||
const apiBaseURL = `http://${apiHost}:${apiPort}`;
|
||||
const enterpriseMode = process.argv.includes('--enterprise');
|
||||
const enterpriseMode = process.argv.includes("--enterprise");
|
||||
const deploymentEnv = process.env.GOCHAT_SMOKE_DEPLOYMENT_ENV || "cloud";
|
||||
const installationName = process.env.GOCHAT_SMOKE_INSTALLATION_NAME || "GoChat";
|
||||
const expectedBilling =
|
||||
(process.env.GOCHAT_SMOKE_EXPECT_BILLING ||
|
||||
(deploymentEnv === "cloud" && installationName === "GoChat"
|
||||
? "true"
|
||||
: "false")) === "true";
|
||||
|
||||
function isSuccessfulRequest(request, substring) {
|
||||
return request.url.includes(substring) && request.status >= 200 && request.status < 400;
|
||||
return (
|
||||
request.url.includes(substring) &&
|
||||
request.status >= 200 &&
|
||||
request.status < 400
|
||||
);
|
||||
}
|
||||
|
||||
function isFailedAPIRequest(request) {
|
||||
const isBackendAPI = request.url.includes(apiBaseURL);
|
||||
const isShellProxiedAPI = request.url.startsWith(frontendBaseURL) && (
|
||||
request.url.includes('/api/') ||
|
||||
request.url.includes('/enterprise/') ||
|
||||
request.url.includes('/public/') ||
|
||||
request.url.includes('/auth/') ||
|
||||
request.url.includes('/rails/')
|
||||
);
|
||||
if (!isBackendAPI && !isShellProxiedAPI || request.type === 'Preflight') return false;
|
||||
const isNavigationAbort = request.type === 'Document' &&
|
||||
['net::ERR_ABORTED', 'net::ERR_FAILED'].includes(request.errorText);
|
||||
return request.status >= 400 || request.status === 0 && !isNavigationAbort;
|
||||
const isShellProxiedAPI =
|
||||
request.url.startsWith(frontendBaseURL) &&
|
||||
(request.url.includes("/api/") ||
|
||||
request.url.includes("/enterprise/") ||
|
||||
request.url.includes("/public/") ||
|
||||
request.url.includes("/auth/") ||
|
||||
request.url.includes("/rails/"));
|
||||
if ((!isBackendAPI && !isShellProxiedAPI) || request.type === "Preflight")
|
||||
return false;
|
||||
const isNavigationAbort =
|
||||
request.type === "Document" &&
|
||||
["net::ERR_ABORTED", "net::ERR_FAILED"].includes(request.errorText);
|
||||
return request.status >= 400 || (request.status === 0 && !isNavigationAbort);
|
||||
}
|
||||
|
||||
if (process.argv.includes('--self-test')) {
|
||||
if (process.argv.includes("--self-test")) {
|
||||
const apiURL = `${apiBaseURL}/api/v1/profile`;
|
||||
const checks = [
|
||||
isSuccessfulRequest({ url: apiURL, status: 200 }, '/api/v1/profile'),
|
||||
!isSuccessfulRequest({ url: apiURL, status: 500 }, '/api/v1/profile'),
|
||||
isFailedAPIRequest({ url: apiURL, status: 0, type: 'Fetch', errorText: 'net::ERR_FAILED' }),
|
||||
!isFailedAPIRequest({ url: apiURL, status: 0, type: 'Document', errorText: 'net::ERR_ABORTED' }),
|
||||
!isFailedAPIRequest({ url: `${frontendBaseURL}/favicon.ico`, status: 404, type: 'Image' }),
|
||||
isSuccessfulRequest({ url: apiURL, status: 200 }, "/api/v1/profile"),
|
||||
!isSuccessfulRequest({ url: apiURL, status: 500 }, "/api/v1/profile"),
|
||||
isFailedAPIRequest({
|
||||
url: apiURL,
|
||||
status: 0,
|
||||
type: "Fetch",
|
||||
errorText: "net::ERR_FAILED",
|
||||
}),
|
||||
!isFailedAPIRequest({
|
||||
url: apiURL,
|
||||
status: 0,
|
||||
type: "Document",
|
||||
errorText: "net::ERR_ABORTED",
|
||||
}),
|
||||
!isFailedAPIRequest({
|
||||
url: `${frontendBaseURL}/favicon.ico`,
|
||||
status: 404,
|
||||
type: "Image",
|
||||
}),
|
||||
];
|
||||
if (checks.some(check => !check)) throw new Error('browser smoke failure contract self-test failed');
|
||||
console.log('browser smoke failure contract self-test: ok');
|
||||
if (checks.some((check) => !check))
|
||||
throw new Error("browser smoke failure contract self-test failed");
|
||||
console.log("browser smoke failure contract self-test: ok");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
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 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',
|
||||
mode: enterpriseMode ? "enterprise" : "core",
|
||||
frontend_dir: frontendDir,
|
||||
frontend_base_url: frontendBaseURL,
|
||||
api_base_url: apiBaseURL,
|
||||
account_id: seed.account_id,
|
||||
installation_name: installationName,
|
||||
requests: [],
|
||||
console: [],
|
||||
checks: [],
|
||||
@@ -78,31 +118,28 @@ const report = {
|
||||
|
||||
function smokeHTML(entrypoint, route) {
|
||||
const config = {
|
||||
apiHost: '',
|
||||
apiHost: "",
|
||||
hostURL: frontendBaseURL,
|
||||
helpCenterURL: '',
|
||||
allowedLoginMethods: ['email'],
|
||||
signupEnabled: 'false',
|
||||
isMfaEnabled: 'false',
|
||||
enabledLanguages: [{ iso_639_1_code: 'en', name: 'English' }],
|
||||
helpCenterURL: "",
|
||||
allowedLoginMethods: ["email"],
|
||||
signupEnabled: "false",
|
||||
isMfaEnabled: "false",
|
||||
enabledLanguages: [{ iso_639_1_code: "en", name: "English" }],
|
||||
helpUrls: {},
|
||||
selectedLocale: 'en',
|
||||
isEnterprise: 'true',
|
||||
enterprisePlanName: 'enterprise',
|
||||
selectedLocale: "en",
|
||||
};
|
||||
const globalConfig = {
|
||||
INSTALLATION_NAME: 'GoChat',
|
||||
BRAND_NAME: 'GoChat',
|
||||
LOGO: '/logo.png',
|
||||
LOGO_DARK: '',
|
||||
LOGO_THUMBNAIL: '/logo.png',
|
||||
DISABLE_USER_PROFILE_UPDATE: 'false',
|
||||
DIRECT_UPLOADS_ENABLED: 'false',
|
||||
MAXIMUM_FILE_UPLOAD_SIZE: '40',
|
||||
INSTALLATION_NAME: installationName,
|
||||
BRAND_NAME: "GoChat",
|
||||
LOGO: "/logo.png",
|
||||
LOGO_DARK: "",
|
||||
LOGO_THUMBNAIL: "/logo.png",
|
||||
DISABLE_USER_PROFILE_UPDATE: "false",
|
||||
DIRECT_UPLOADS_ENABLED: "false",
|
||||
MAXIMUM_FILE_UPLOAD_SIZE: "40",
|
||||
ACTIVE_PLATFORM_BANNERS: [],
|
||||
LOGOUT_REDIRECT_LINK: '/app/login',
|
||||
DEPLOYMENT_ENV: 'cloud',
|
||||
IS_ENTERPRISE: 'true',
|
||||
LOGOUT_REDIRECT_LINK: "/app/login",
|
||||
DEPLOYMENT_ENV: deploymentEnv,
|
||||
};
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
@@ -112,6 +149,7 @@ function smokeHTML(entrypoint, route) {
|
||||
<title>GoChat Smoke</title>
|
||||
<script>
|
||||
history.replaceState({}, '', ${JSON.stringify(route)});
|
||||
window.__GOCHAT_CONFIG__ = ${JSON.stringify({ ...config, globalConfig })};
|
||||
window.chatwootConfig = ${JSON.stringify(config)};
|
||||
window.globalConfig = ${JSON.stringify(globalConfig)};
|
||||
window.browserConfig = { browser_name: 'chrome' };
|
||||
@@ -131,8 +169,8 @@ function widgetSmokeHTML(route) {
|
||||
const chatwootWebChannel = {
|
||||
...websiteChannelConfig,
|
||||
websiteToken: websiteChannelConfig.website_token,
|
||||
enabledLanguages: [{ iso_639_1_code: 'en', name: 'English' }],
|
||||
locale: 'en',
|
||||
enabledLanguages: [{ iso_639_1_code: "en", name: "English" }],
|
||||
locale: "en",
|
||||
portal: null,
|
||||
hasAConnectedAgentBot: false,
|
||||
allowMessagesAfterResolved: true,
|
||||
@@ -147,8 +185,8 @@ function widgetSmokeHTML(route) {
|
||||
<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.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' };
|
||||
@@ -175,166 +213,198 @@ function writeWidgetSmokeShell(name, route) {
|
||||
function startSmokeShellServer() {
|
||||
const server = createServer(async (req, res) => {
|
||||
try {
|
||||
const requestURL = new URL(req.url || '/', frontendBaseURL);
|
||||
const requestURL = new URL(req.url || "/", frontendBaseURL);
|
||||
if (smokeShells.has(requestURL.pathname)) {
|
||||
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
||||
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' });
|
||||
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 {};');
|
||||
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 upstream = await fetch(
|
||||
`${viteBaseURL}${requestURL.pathname}${requestURL.search}`,
|
||||
);
|
||||
const headers = Object.fromEntries(upstream.headers.entries());
|
||||
headers['access-control-allow-origin'] = '*';
|
||||
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('/enterprise/') ||
|
||||
requestURL.pathname.startsWith('/public/') ||
|
||||
requestURL.pathname.startsWith('/auth/') ||
|
||||
requestURL.pathname.startsWith('/rails/')
|
||||
requestURL.pathname.startsWith("/api/") ||
|
||||
requestURL.pathname.startsWith("/enterprise/") ||
|
||||
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 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'] = '*';
|
||||
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());');
|
||||
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') {
|
||||
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');
|
||||
res.writeHead(404, { "content-type": "text/plain" });
|
||||
res.end("not found");
|
||||
} catch (error) {
|
||||
res.writeHead(502, { 'content-type': 'text/plain' });
|
||||
res.writeHead(502, { "content-type": "text/plain" });
|
||||
res.end(error.message);
|
||||
}
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.once("error", reject);
|
||||
server.listen(Number(shellPort), shellHost, () => resolve(server));
|
||||
});
|
||||
}
|
||||
|
||||
const smokePages = {
|
||||
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`),
|
||||
dashboard: writeSmokeShell(
|
||||
"gochat-smoke-dashboard",
|
||||
"dashboard",
|
||||
`/app/accounts/${seed.account_id}/dashboard`,
|
||||
),
|
||||
billing: `${frontendBaseURL}/app/accounts/${seed.account_id}/settings/billing`,
|
||||
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 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',
|
||||
"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',
|
||||
name: 'gochat-smoke-enterprise-sla-reports',
|
||||
label: "SLA reports screen",
|
||||
name: "gochat-smoke-enterprise-sla-reports",
|
||||
route: `/app/accounts/${seed.account_id}/reports/sla`,
|
||||
requests: ['/applied_slas', '/applied_slas/metrics'],
|
||||
requests: ["/applied_slas", "/applied_slas/metrics"],
|
||||
},
|
||||
{
|
||||
label: 'CSAT reports screen',
|
||||
name: 'gochat-smoke-enterprise-csat-reports',
|
||||
label: "CSAT reports screen",
|
||||
name: "gochat-smoke-enterprise-csat-reports",
|
||||
route: `/app/accounts/${seed.account_id}/reports/csat`,
|
||||
requests: ['/csat_survey_responses', '/csat_survey_responses/metrics'],
|
||||
requests: ["/csat_survey_responses", "/csat_survey_responses/metrics"],
|
||||
},
|
||||
{
|
||||
label: 'automation rules screen',
|
||||
name: 'gochat-smoke-enterprise-automation',
|
||||
label: "automation rules screen",
|
||||
name: "gochat-smoke-enterprise-automation",
|
||||
route: `/app/accounts/${seed.account_id}/settings/automation/list`,
|
||||
requests: ['/automation_rules'],
|
||||
requests: ["/automation_rules"],
|
||||
},
|
||||
{
|
||||
label: 'macros screen',
|
||||
name: 'gochat-smoke-enterprise-macros',
|
||||
label: "macros screen",
|
||||
name: "gochat-smoke-enterprise-macros",
|
||||
route: `/app/accounts/${seed.account_id}/settings/macros`,
|
||||
requests: ['/macros'],
|
||||
requests: ["/macros"],
|
||||
},
|
||||
{
|
||||
label: 'audit logs screen',
|
||||
name: 'gochat-smoke-enterprise-audit-logs',
|
||||
label: "audit logs screen",
|
||||
name: "gochat-smoke-enterprise-audit-logs",
|
||||
route: `/app/accounts/${seed.account_id}/settings/audit-logs/list`,
|
||||
requests: ['/audit_logs'],
|
||||
requests: ["/audit_logs"],
|
||||
},
|
||||
{
|
||||
label: 'custom roles screen',
|
||||
name: 'gochat-smoke-enterprise-custom-roles',
|
||||
label: "custom roles screen",
|
||||
name: "gochat-smoke-enterprise-custom-roles",
|
||||
route: `/app/accounts/${seed.account_id}/settings/custom-roles/list`,
|
||||
requests: ['/custom_roles'],
|
||||
requests: ["/custom_roles"],
|
||||
},
|
||||
{
|
||||
label: 'notifications screen',
|
||||
name: 'gochat-smoke-enterprise-notifications',
|
||||
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',
|
||||
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`,
|
||||
'/api/v1/profile/sessions',
|
||||
"/api/v1/profile/sessions",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'agent capacity screen',
|
||||
name: 'gochat-smoke-enterprise-agent-capacity',
|
||||
label: "agent capacity screen",
|
||||
name: "gochat-smoke-enterprise-agent-capacity",
|
||||
route: `/app/accounts/${seed.account_id}/settings/assignment-policy/capacity`,
|
||||
requests: ['/agent_capacity_policies'],
|
||||
requests: ["/agent_capacity_policies"],
|
||||
},
|
||||
{
|
||||
label: 'agent capacity edit screen',
|
||||
name: 'gochat-smoke-enterprise-agent-capacity-edit',
|
||||
label: "agent capacity edit screen",
|
||||
name: "gochat-smoke-enterprise-agent-capacity-edit",
|
||||
route: `/app/accounts/${seed.account_id}/settings/assignment-policy/capacity/edit/${seed.capacity_policy_id}`,
|
||||
requests: [`/agent_capacity_policies/${seed.capacity_policy_id}`, `/agent_capacity_policies/${seed.capacity_policy_id}/users`],
|
||||
requests: [
|
||||
`/agent_capacity_policies/${seed.capacity_policy_id}`,
|
||||
`/agent_capacity_policies/${seed.capacity_policy_id}/users`,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Captain settings screen',
|
||||
name: 'gochat-smoke-enterprise-captain-settings',
|
||||
label: "Captain settings screen",
|
||||
name: "gochat-smoke-enterprise-captain-settings",
|
||||
route: `/app/accounts/${seed.account_id}/settings/captain`,
|
||||
requests: ['/captain/preferences'],
|
||||
requests: ["/captain/preferences"],
|
||||
},
|
||||
{
|
||||
label: 'Captain assistant overview screen',
|
||||
name: 'gochat-smoke-enterprise-captain-overview',
|
||||
label: "Captain assistant overview screen",
|
||||
name: "gochat-smoke-enterprise-captain-overview",
|
||||
route: `/app/accounts/${seed.account_id}/captain/${seed.captain_assistant_id}/overview`,
|
||||
requests: [
|
||||
`/captain/assistants/${seed.captain_assistant_id}/stats`,
|
||||
@@ -342,8 +412,8 @@ const enterprisePages = [
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'billing screen',
|
||||
name: 'gochat-smoke-enterprise-billing',
|
||||
label: "billing screen",
|
||||
name: "gochat-smoke-enterprise-billing",
|
||||
route: `/app/accounts/${seed.account_id}/settings/billing`,
|
||||
requests: [
|
||||
`/enterprise/api/v1/accounts/${seed.account_id}/subscription`,
|
||||
@@ -351,14 +421,14 @@ const enterprisePages = [
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'voice inbox settings screen',
|
||||
name: 'gochat-smoke-enterprise-voice-inbox',
|
||||
label: "voice inbox settings screen",
|
||||
name: "gochat-smoke-enterprise-voice-inbox",
|
||||
route: `/app/accounts/${seed.account_id}/settings/inboxes/${seed.voice_inbox_id}/voice-configuration`,
|
||||
requests: [`/api/v1/accounts/${seed.account_id}/inboxes`],
|
||||
},
|
||||
].map(page => ({
|
||||
].map((page) => ({
|
||||
...page,
|
||||
url: writeSmokeShell(page.name, 'dashboard', page.route),
|
||||
url: writeSmokeShell(page.name, "dashboard", page.route),
|
||||
}));
|
||||
|
||||
class CDPPage {
|
||||
@@ -369,7 +439,7 @@ class CDPPage {
|
||||
this.pending = new Map();
|
||||
this.listeners = new Map();
|
||||
this.requestURLs = new Map();
|
||||
ws.onmessage = event => this.handleMessage(JSON.parse(event.data));
|
||||
ws.onmessage = (event) => this.handleMessage(JSON.parse(event.data));
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
@@ -381,7 +451,7 @@ class CDPPage {
|
||||
return;
|
||||
}
|
||||
const handlers = this.listeners.get(message.method) || [];
|
||||
handlers.forEach(handler => handler(message.params || {}));
|
||||
handlers.forEach((handler) => handler(message.params || {}));
|
||||
}
|
||||
|
||||
send(method, params = {}) {
|
||||
@@ -405,31 +475,36 @@ class CDPPage {
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.send('Page.enable');
|
||||
await this.send('Runtime.enable');
|
||||
await this.send('Network.enable');
|
||||
this.on('Network.requestWillBeSent', params => {
|
||||
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 => {
|
||||
this.on("Runtime.consoleAPICalled", (params) => {
|
||||
report.console.push({
|
||||
type: params.type,
|
||||
text: (params.args || []).map(arg => arg.value || arg.description || '').join(' '),
|
||||
text: (params.args || [])
|
||||
.map((arg) => arg.value || arg.description || "")
|
||||
.join(" "),
|
||||
});
|
||||
});
|
||||
this.on('Runtime.exceptionThrown', params => {
|
||||
report.console.push({ type: 'exception', text: params.exceptionDetails?.text || 'exception' });
|
||||
this.on("Runtime.exceptionThrown", (params) => {
|
||||
report.console.push({
|
||||
type: "exception",
|
||||
text: params.exceptionDetails?.text || "exception",
|
||||
});
|
||||
});
|
||||
this.on('Network.responseReceived', params => {
|
||||
this.on("Network.responseReceived", (params) => {
|
||||
report.requests.push({
|
||||
url: params.response.url,
|
||||
status: params.response.status,
|
||||
type: params.type,
|
||||
});
|
||||
});
|
||||
this.on('Network.loadingFailed', params => {
|
||||
this.on("Network.loadingFailed", (params) => {
|
||||
report.requests.push({
|
||||
url: this.requestURLs.get(params.requestId) || params.requestId,
|
||||
status: 0,
|
||||
@@ -440,19 +515,23 @@ class CDPPage {
|
||||
}
|
||||
|
||||
async navigate(url) {
|
||||
const loaded = new Promise(resolve => this.on('Page.loadEventFired', resolve));
|
||||
await this.send('Page.navigate', { url });
|
||||
const loaded = new Promise((resolve) =>
|
||||
this.on("Page.loadEventFired", resolve),
|
||||
);
|
||||
await this.send("Page.navigate", { url });
|
||||
await loaded;
|
||||
}
|
||||
|
||||
async eval(expression) {
|
||||
const result = await this.send('Runtime.evaluate', {
|
||||
const result = await this.send("Runtime.evaluate", {
|
||||
expression,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
});
|
||||
if (result.exceptionDetails) {
|
||||
throw new Error(result.exceptionDetails.text || 'Runtime.evaluate failed');
|
||||
throw new Error(
|
||||
result.exceptionDetails.text || "Runtime.evaluate failed",
|
||||
);
|
||||
}
|
||||
return result.result?.value;
|
||||
}
|
||||
@@ -461,10 +540,10 @@ class CDPPage {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
if (await this.eval(expression)) {
|
||||
report.checks.push({ label, status: 'passed' });
|
||||
report.checks.push({ label, status: "passed" });
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${label}`);
|
||||
}
|
||||
@@ -474,7 +553,7 @@ class CDPPage {
|
||||
await this.waitFor(
|
||||
'document.querySelector("#app") && document.querySelector("#app").children.length > 0',
|
||||
label,
|
||||
timeout
|
||||
timeout,
|
||||
);
|
||||
} catch (error) {
|
||||
const state = await this.eval(`JSON.stringify({
|
||||
@@ -484,7 +563,7 @@ class CDPPage {
|
||||
appChildCount: document.querySelector('#app')?.children?.length || 0,
|
||||
title: document.title,
|
||||
})`);
|
||||
report.console.push({ type: 'diagnostic', text: `${label}: ${state}` });
|
||||
report.console.push({ type: "diagnostic", text: `${label}: ${state}` });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -493,7 +572,7 @@ class CDPPage {
|
||||
return this.waitFor(
|
||||
`performance.getEntriesByType('resource').some(entry => entry.name.includes(${JSON.stringify(substring)}))`,
|
||||
label,
|
||||
timeout
|
||||
timeout,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -501,65 +580,88 @@ class CDPPage {
|
||||
return this.waitForSuccessfulRequestAfter(substring, 0, label, timeout);
|
||||
}
|
||||
|
||||
async waitForSuccessfulRequestAfter(substring, requestIndex, label, timeout = 30000) {
|
||||
async waitForSuccessfulRequestAfter(
|
||||
substring,
|
||||
requestIndex,
|
||||
label,
|
||||
timeout = 30000,
|
||||
) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
if (report.requests.slice(requestIndex).some(request => isSuccessfulRequest(request, substring))) {
|
||||
report.checks.push({ label, status: 'passed' });
|
||||
if (
|
||||
report.requests
|
||||
.slice(requestIndex)
|
||||
.some((request) => isSuccessfulRequest(request, substring))
|
||||
) {
|
||||
report.checks.push({ label, status: "passed" });
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
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(isFailedAPIRequest);
|
||||
const failures = report.requests
|
||||
.slice(requestIndex)
|
||||
.filter(isFailedAPIRequest);
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`Frontend API requests failed: ${failures.map(request => `${request.status} ${request.url}`).join('; ')}`);
|
||||
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' });
|
||||
report.checks.push({
|
||||
label: "no failed frontend API requests",
|
||||
status: "passed",
|
||||
});
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.ws.close();
|
||||
this.chrome.kill('SIGTERM');
|
||||
this.chrome.kill("SIGTERM");
|
||||
}
|
||||
}
|
||||
|
||||
async function launchChrome() {
|
||||
if (typeof WebSocket !== 'function') {
|
||||
throw new Error('Node.js WebSocket API is unavailable; use --experimental-websocket');
|
||||
if (typeof WebSocket !== "function") {
|
||||
throw new Error(
|
||||
"Node.js WebSocket API is unavailable; use --experimental-websocket",
|
||||
);
|
||||
}
|
||||
const userDataDir = mkdtempSync(path.join(tmpdir(), 'gochat-chrome-'));
|
||||
const userDataDir = mkdtempSync(path.join(tmpdir(), "gochat-chrome-"));
|
||||
const args = [
|
||||
'--headless=new',
|
||||
'--disable-gpu',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-dev-shm-usage',
|
||||
'--enable-logging=stderr',
|
||||
'--remote-debugging-port=0',
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-dev-shm-usage",
|
||||
"--enable-logging=stderr",
|
||||
"--remote-debugging-port=0",
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
];
|
||||
if (process.env.CI) args.push('--no-sandbox');
|
||||
args.push('about:blank');
|
||||
const chromeLogPath = path.join(logDir, 'chrome.log');
|
||||
const chromeLog = openSync(chromeLogPath, 'w');
|
||||
const chrome = spawn(chromePath, args, { stdio: ['ignore', 'ignore', chromeLog] });
|
||||
if (process.env.CI) args.push("--no-sandbox");
|
||||
args.push("about:blank");
|
||||
const chromeLogPath = path.join(logDir, "chrome.log");
|
||||
const chromeLog = openSync(chromeLogPath, "w");
|
||||
const chrome = spawn(chromePath, args, {
|
||||
stdio: ["ignore", "ignore", chromeLog],
|
||||
});
|
||||
closeSync(chromeLog);
|
||||
let chromeFailure = '';
|
||||
chrome.once('error', error => {
|
||||
let chromeFailure = "";
|
||||
chrome.once("error", (error) => {
|
||||
chromeFailure = `failed to launch Chrome: ${error.message}`;
|
||||
});
|
||||
chrome.once('exit', (code, signal) => {
|
||||
chrome.once("exit", (code, signal) => {
|
||||
chromeFailure = `Chrome exited before DevTools was ready (code=${code}, signal=${signal})`;
|
||||
});
|
||||
const portFile = path.join(userDataDir, 'DevToolsActivePort');
|
||||
const portFile = path.join(userDataDir, "DevToolsActivePort");
|
||||
for (let i = 0; i < 80; i += 1) {
|
||||
try {
|
||||
const [port] = readFileSync(portFile, 'utf8').trim().split('\n');
|
||||
const target = await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT' }).then(r => r.json());
|
||||
const [port] = readFileSync(portFile, "utf8").trim().split("\n");
|
||||
const target = await fetch(
|
||||
`http://127.0.0.1:${port}/json/new?about:blank`,
|
||||
{ method: "PUT" },
|
||||
).then((r) => r.json());
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onopen = resolve;
|
||||
@@ -570,37 +672,80 @@ async function launchChrome() {
|
||||
return page;
|
||||
} catch {
|
||||
if (chromeFailure) break;
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
}
|
||||
chrome.kill('SIGTERM');
|
||||
throw new Error(`${chromeFailure || 'Chrome DevTools did not become ready'}; see ${chromeLogPath}`);
|
||||
chrome.kill("SIGTERM");
|
||||
throw new Error(
|
||||
`${chromeFailure || "Chrome DevTools did not become ready"}; see ${chromeLogPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const shellServer = await startSmokeShellServer();
|
||||
const page = await launchChrome();
|
||||
try {
|
||||
await page.send('Network.setCookie', {
|
||||
name: 'cw_d_session_info',
|
||||
await page.send("Network.setCookie", {
|
||||
name: "cw_d_session_info",
|
||||
value: encodeURIComponent(sessionCookie),
|
||||
url: frontendBaseURL,
|
||||
path: '/',
|
||||
path: "/",
|
||||
});
|
||||
|
||||
await page.navigate(smokePages.dashboard);
|
||||
await page.waitForAppMounted('dashboard app mounted');
|
||||
await page.waitForSuccessfulRequest('/auth/validate_token', 'dashboard validates auth token');
|
||||
await page.waitForSuccessfulRequest(`/api/v1/accounts/${seed.account_id}/conversations`, 'dashboard requests conversations');
|
||||
await page.waitForAppMounted("dashboard app mounted");
|
||||
await page.waitForSuccessfulRequest(
|
||||
"/auth/validate_token",
|
||||
"dashboard validates auth token",
|
||||
);
|
||||
await page.waitForSuccessfulRequest(
|
||||
`/api/v1/accounts/${seed.account_id}/conversations`,
|
||||
"dashboard requests conversations",
|
||||
);
|
||||
page.assertNoFailedAPIRequests();
|
||||
|
||||
const billingLinkVisible = await page.eval(
|
||||
`Boolean(document.querySelector('a[href*="/settings/billing"]'))`,
|
||||
);
|
||||
if (billingLinkVisible !== expectedBilling) {
|
||||
throw new Error(
|
||||
`Billing sidebar visibility mismatch: expected ${expectedBilling}, got ${billingLinkVisible}`,
|
||||
);
|
||||
}
|
||||
report.checks.push({
|
||||
label: `Billing sidebar visibility (${deploymentEnv})`,
|
||||
status: "passed",
|
||||
});
|
||||
|
||||
const billingRequestIndex = report.requests.length;
|
||||
await page.navigate(smokePages.billing);
|
||||
await page.waitForAppMounted("billing route app mounted");
|
||||
await page.waitFor(
|
||||
`location.pathname.includes('/settings/billing') === ${expectedBilling}`,
|
||||
`Billing direct-route guard (${deploymentEnv})`,
|
||||
);
|
||||
page.assertNoFailedAPIRequests(billingRequestIndex);
|
||||
await page.navigate(smokePages.dashboard);
|
||||
await page.waitForAppMounted("dashboard remounted after billing route");
|
||||
|
||||
const widgetRequestIndex = report.requests.length;
|
||||
await page.navigate(smokePages.widget);
|
||||
await page.waitForAppMounted('widget app mounted');
|
||||
await page.waitForSuccessfulRequest('/api/v1/widget/messages', 'widget requests messages');
|
||||
await page.waitForSuccessfulRequest('/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.waitForSuccessfulRequest('/api/v1/widget/campaigns', 'widget campaigns endpoint works');
|
||||
await page.waitForAppMounted("widget app mounted");
|
||||
await page.waitForSuccessfulRequest(
|
||||
"/api/v1/widget/messages",
|
||||
"widget requests messages",
|
||||
);
|
||||
await page.waitForSuccessfulRequest(
|
||||
"/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.waitForSuccessfulRequest(
|
||||
"/api/v1/widget/campaigns",
|
||||
"widget campaigns endpoint works",
|
||||
);
|
||||
page.assertNoFailedAPIRequests(widgetRequestIndex);
|
||||
|
||||
if (enterpriseMode) {
|
||||
@@ -609,7 +754,11 @@ async function main() {
|
||||
await page.navigate(enterprisePage.url);
|
||||
await page.waitForAppMounted(`${enterprisePage.label} app mounted`);
|
||||
for (const request of enterprisePage.requests) {
|
||||
await page.waitForSuccessfulRequestAfter(request, requestIndex, `${enterprisePage.label} requests ${request}`);
|
||||
await page.waitForSuccessfulRequestAfter(
|
||||
request,
|
||||
requestIndex,
|
||||
`${enterprisePage.label} requests ${request}`,
|
||||
);
|
||||
}
|
||||
page.assertNoFailedAPIRequests(requestIndex);
|
||||
}
|
||||
@@ -629,24 +778,43 @@ async function main() {
|
||||
})(),
|
||||
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.waitForSuccessfulRequestAfter('/captain/copilot_threads', copilotRequestIndex, 'browser context requests Copilot threads');
|
||||
await page.waitForSuccessfulRequestAfter(
|
||||
"/captain/copilot_threads",
|
||||
copilotRequestIndex,
|
||||
"browser context requests Copilot threads",
|
||||
);
|
||||
page.assertNoFailedAPIRequests(copilotRequestIndex);
|
||||
}
|
||||
const runtimeExceptions = report.console.filter(
|
||||
(entry) => entry.type === "exception",
|
||||
);
|
||||
if (runtimeExceptions.length > 0) {
|
||||
throw new Error(
|
||||
`Browser runtime exceptions: ${runtimeExceptions.map((entry) => entry.text).join("; ")}`,
|
||||
);
|
||||
}
|
||||
report.checks.push({
|
||||
label: "no browser runtime exceptions",
|
||||
status: "passed",
|
||||
});
|
||||
report.finished_at = new Date().toISOString();
|
||||
report.status = 'passed';
|
||||
report.status = "passed";
|
||||
} catch (error) {
|
||||
report.finished_at = new Date().toISOString();
|
||||
report.status = 'failed';
|
||||
report.status = "failed";
|
||||
report.error = error.message;
|
||||
throw error;
|
||||
} finally {
|
||||
writeFileSync(path.join(logDir, 'browser-smoke-report.json'), JSON.stringify(report, null, 2));
|
||||
writeFileSync(
|
||||
path.join(logDir, "browser-smoke-report.json"),
|
||||
JSON.stringify(report, null, 2),
|
||||
);
|
||||
await page.close();
|
||||
await new Promise(resolve => shellServer.close(resolve));
|
||||
await new Promise((resolve) => shellServer.close(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Repeatable B12 smoke harness for running the reused Chatwoot frontend against GoChat.
|
||||
# Repeatable B12 smoke harness for running the GoChat frontend against GoChat.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CHATWOOT_DIR="${CHATWOOT_DIR:-$ROOT/../docs/chatwoot}"
|
||||
CHATWOOT_DIR="${CHATWOOT_DIR:-$ROOT/../frontend}"
|
||||
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}"
|
||||
@@ -26,14 +26,14 @@ Modes:
|
||||
--check Validate local prerequisites and write a readiness report.
|
||||
--print Print the exact boot commands without starting processes.
|
||||
--boot-only Start GoChat and Vite, verify /health and frontend HTTP, then exit.
|
||||
--api-smoke Run seed plus Chatwoot-frontend API assertions against a running GoChat backend.
|
||||
--browser-smoke Run API smoke, then drive the reused Chatwoot frontend in headless Chrome.
|
||||
--api-smoke Run seed plus GoChat-frontend API assertions against a running GoChat backend.
|
||||
--browser-smoke Run API smoke, then drive the GoChat frontend in headless Chrome.
|
||||
--enterprise-smoke Run API smoke plus enterprise feature API assertions against GoChat.
|
||||
--enterprise-browser-smoke Run enterprise smoke, then navigate reused Chatwoot enterprise screens in headless Chrome.
|
||||
--enterprise-browser-smoke Run enterprise smoke, then navigate GoChat enterprise screens in headless Chrome.
|
||||
--no-seed Skip seed during --api-smoke and use GOCHAT_SEED_* values already present in DB.
|
||||
|
||||
Environment:
|
||||
CHATWOOT_DIR Chatwoot checkout path. Default: ../docs/chatwoot (4.15.1 upstream source)
|
||||
CHATWOOT_DIR Frontend checkout path. Default: ../frontend
|
||||
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
|
||||
@@ -44,6 +44,9 @@ Environment:
|
||||
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
|
||||
GOCHAT_SMOKE_DEPLOYMENT_ENV Browser fixture deployment environment. Default: cloud
|
||||
GOCHAT_SMOKE_INSTALLATION_NAME Browser fixture installation name. Default: GoChat
|
||||
GOCHAT_SMOKE_EXPECT_BILLING Expected Billing visibility (true/false); defaults from cloud/custom-brand matrix.
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -51,21 +54,28 @@ RUN_SEED="true"
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--check) MODE="check" ;;
|
||||
--print) MODE="print" ;;
|
||||
--boot-only) KEEP_ALIVE="false" ;;
|
||||
--api-smoke) MODE="api-smoke" ;;
|
||||
--browser-smoke) MODE="browser-smoke" ;;
|
||||
--enterprise-smoke) MODE="enterprise-smoke" ;;
|
||||
--enterprise-browser-smoke) MODE="enterprise-browser-smoke" ;;
|
||||
--no-seed) RUN_SEED="false" ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown argument: $arg" >&2; usage; exit 2 ;;
|
||||
--check) MODE="check" ;;
|
||||
--print) MODE="print" ;;
|
||||
--boot-only) KEEP_ALIVE="false" ;;
|
||||
--api-smoke) MODE="api-smoke" ;;
|
||||
--browser-smoke) MODE="browser-smoke" ;;
|
||||
--enterprise-smoke) MODE="enterprise-smoke" ;;
|
||||
--enterprise-browser-smoke) MODE="enterprise-browser-smoke" ;;
|
||||
--no-seed) RUN_SEED="false" ;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $arg" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
backend_cmd=(env GOCHAT_ENV=development GOCHAT_SERVER_HOST="$API_HOST" GOCHAT_SERVER_PORT="$API_PORT" GOCHAT_SERVER_MODE=debug GOCHAT_SEARCH_ENGINE="$SEARCH_ENGINE" GOCHAT_SEARCH_HOST="$MEILI_HOST" GOCHAT_SEARCH_API_KEY="$MEILI_API_KEY" go run ./cmd/gochat serve)
|
||||
frontend_cmd=(env CHATWOOT_API_HOST="http://$API_HOST:$API_PORT" pnpm exec vite --host "$FRONTEND_HOST" --port "$FRONTEND_PORT")
|
||||
frontend_cmd=(env VITE_API_HOST="http://$API_HOST:$API_PORT" pnpm exec vite --host "$FRONTEND_HOST" --port "$FRONTEND_PORT")
|
||||
|
||||
need() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
@@ -124,10 +134,11 @@ scripts/parity_frontend_smoke.sh --enterprise-browser-smoke
|
||||
- Search: \`$SEARCH_ENGINE\` at \`$MEILI_HOST\`
|
||||
- Log: \`$LOG_DIR/gochat.log\`
|
||||
|
||||
## Reused Chatwoot Frontend
|
||||
## GoChat Frontend
|
||||
|
||||
- URL: http://$FRONTEND_HOST:$FRONTEND_PORT
|
||||
- Source: \`$CHATWOOT_DIR\`
|
||||
- Deployment fixture: \`${GOCHAT_SMOKE_DEPLOYMENT_ENV:-cloud}\`
|
||||
- Command: \`(cd $CHATWOOT_DIR && ${frontend_cmd[*]})\`
|
||||
- Log: \`$LOG_DIR/chatwoot-vite.log\`
|
||||
|
||||
@@ -148,7 +159,7 @@ The seed command creates deterministic login/account/inbox/contact/company/conve
|
||||
| Area | Current result | Owner if failing |
|
||||
| --- | --- | --- |
|
||||
| Boot GoChat backend | ${BOOT_BACKEND_RESULT:-Not run in check mode} | B12.1 |
|
||||
| Boot reused Chatwoot Vite frontend | ${BOOT_FRONTEND_RESULT:-Not run in check mode} | B12.1 |
|
||||
| Boot GoChat Vite frontend | ${BOOT_FRONTEND_RESULT:-Not run in check mode} | B12.1 |
|
||||
| Auth/profile | ${AUTH_PROFILE_RESULT:-Pending browser/API smoke} | B2/B12.2 |
|
||||
| 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 |
|
||||
@@ -173,9 +184,18 @@ check_prereqs() {
|
||||
need curl
|
||||
need node
|
||||
need pnpm
|
||||
test -d "$CHATWOOT_DIR" || { echo "missing Chatwoot checkout: $CHATWOOT_DIR" >&2; return 1; }
|
||||
test -f "$CHATWOOT_DIR/package.json" || { echo "missing Chatwoot package.json" >&2; return 1; }
|
||||
test -f "$ROOT/cmd/gochat/main.go" || { echo "missing GoChat server entrypoint cmd/gochat/main.go" >&2; return 1; }
|
||||
test -d "$CHATWOOT_DIR" || {
|
||||
echo "missing Chatwoot checkout: $CHATWOOT_DIR" >&2
|
||||
return 1
|
||||
}
|
||||
test -f "$CHATWOOT_DIR/package.json" || {
|
||||
echo "missing Chatwoot package.json" >&2
|
||||
return 1
|
||||
}
|
||||
test -f "$ROOT/cmd/gochat/main.go" || {
|
||||
echo "missing GoChat server entrypoint cmd/gochat/main.go" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
wait_url() {
|
||||
@@ -464,40 +484,40 @@ SEED
|
||||
fi
|
||||
|
||||
BOOT_BACKEND_RESULT="Passed /health during API smoke" \
|
||||
AUTH_PROFILE_RESULT="Passed API smoke" \
|
||||
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" \
|
||||
write_report "API smoke passed for reused Chatwoot frontend core paths; live browser smoke not run in this mode." "Command run: \`scripts/parity_frontend_smoke.sh --api-smoke\`. Logs and payload captures are under \`$LOG_DIR\`. B12.2 still needs browser navigation assertions for the reused Vite app, and B12.3 must add enterprise screen assertions."
|
||||
AUTH_PROFILE_RESULT="Passed API smoke" \
|
||||
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" \
|
||||
write_report "API smoke passed for GoChat frontend core paths; live browser smoke not run in this mode." "Command run: \`scripts/parity_frontend_smoke.sh --api-smoke\`. Logs and payload captures are under \`$LOG_DIR\`. Browser navigation assertions cover the GoChat Vite app; enterprise screen assertions run in enterprise modes."
|
||||
echo "api smoke passed; report written to $REPORT_PATH"
|
||||
}
|
||||
|
||||
run_browser_smoke() {
|
||||
run_api_smoke
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT/app/javascript/entrypoints/dashboard.js" "Chatwoot Vite"
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT/app/javascript/entrypoints/dashboard.js" "GoChat Vite"
|
||||
GOCHAT_ROOT="$ROOT" \
|
||||
CHATWOOT_DIR="$CHATWOOT_DIR" \
|
||||
GOCHAT_SMOKE_LOG_DIR="$LOG_DIR" \
|
||||
GOCHAT_SMOKE_API_HOST="$API_HOST" \
|
||||
GOCHAT_SMOKE_API_PORT="$API_PORT" \
|
||||
GOCHAT_SMOKE_FRONTEND_HOST="$FRONTEND_HOST" \
|
||||
GOCHAT_SMOKE_FRONTEND_PORT="$FRONTEND_PORT" \
|
||||
node --experimental-websocket "$ROOT/scripts/parity_frontend_browser_smoke.mjs"
|
||||
CHATWOOT_DIR="$CHATWOOT_DIR" \
|
||||
GOCHAT_SMOKE_LOG_DIR="$LOG_DIR" \
|
||||
GOCHAT_SMOKE_API_HOST="$API_HOST" \
|
||||
GOCHAT_SMOKE_API_PORT="$API_PORT" \
|
||||
GOCHAT_SMOKE_FRONTEND_HOST="$FRONTEND_HOST" \
|
||||
GOCHAT_SMOKE_FRONTEND_PORT="$FRONTEND_PORT" \
|
||||
node --experimental-websocket "$ROOT/scripts/parity_frontend_browser_smoke.mjs"
|
||||
BOOT_BACKEND_RESULT="Passed /health during browser smoke" \
|
||||
BOOT_FRONTEND_RESULT="Passed Vite readiness during browser smoke" \
|
||||
AUTH_PROFILE_RESULT="Passed browser login plus API 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, 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."
|
||||
BOOT_FRONTEND_RESULT="Passed Vite readiness during browser smoke" \
|
||||
AUTH_PROFILE_RESULT="Passed browser login plus API 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 GoChat login, dashboard, Billing matrix, 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\`."
|
||||
echo "browser smoke passed; report written to $REPORT_PATH"
|
||||
}
|
||||
|
||||
@@ -713,15 +733,15 @@ run_enterprise_api_smoke() {
|
||||
json_assert "$body" 'data.payload && data.payload.length >= 1 && data.payload[0].copilot_thread' "Copilot thread messages return nested payload"
|
||||
|
||||
BOOT_BACKEND_RESULT="Passed /health during enterprise smoke" \
|
||||
AUTH_PROFILE_RESULT="Passed API smoke" \
|
||||
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 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."
|
||||
AUTH_PROFILE_RESULT="Passed API smoke" \
|
||||
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 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 GoChat 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\`."
|
||||
echo "enterprise smoke passed; report written to $REPORT_PATH"
|
||||
}
|
||||
|
||||
@@ -729,24 +749,24 @@ run_enterprise_browser_smoke() {
|
||||
run_enterprise_api_smoke
|
||||
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" \
|
||||
GOCHAT_SMOKE_API_HOST="$API_HOST" \
|
||||
GOCHAT_SMOKE_API_PORT="$API_PORT" \
|
||||
GOCHAT_SMOKE_FRONTEND_HOST="$FRONTEND_HOST" \
|
||||
GOCHAT_SMOKE_FRONTEND_PORT="$FRONTEND_PORT" \
|
||||
node "$ROOT/scripts/parity_frontend_browser_smoke.mjs" --enterprise
|
||||
CHATWOOT_DIR="$CHATWOOT_DIR" \
|
||||
GOCHAT_SMOKE_LOG_DIR="$LOG_DIR" \
|
||||
GOCHAT_SMOKE_API_HOST="$API_HOST" \
|
||||
GOCHAT_SMOKE_API_PORT="$API_PORT" \
|
||||
GOCHAT_SMOKE_FRONTEND_HOST="$FRONTEND_HOST" \
|
||||
GOCHAT_SMOKE_FRONTEND_PORT="$FRONTEND_PORT" \
|
||||
node "$ROOT/scripts/parity_frontend_browser_smoke.mjs" --enterprise
|
||||
BOOT_BACKEND_RESULT="Passed /health during enterprise browser smoke" \
|
||||
BOOT_FRONTEND_RESULT="Passed Vite readiness during enterprise browser smoke" \
|
||||
AUTH_PROFILE_RESULT="Passed browser login plus API 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 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\`."
|
||||
BOOT_FRONTEND_RESULT="Passed Vite readiness during enterprise browser smoke" \
|
||||
AUTH_PROFILE_RESULT="Passed browser login plus API 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 GoChat 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"
|
||||
}
|
||||
|
||||
@@ -791,13 +811,13 @@ echo "starting GoChat backend..."
|
||||
(cd "$ROOT" && "${backend_cmd[@]}") >"$LOG_DIR/gochat.log" 2>&1 &
|
||||
wait_url "http://$API_HOST:$API_PORT/health" "GoChat"
|
||||
|
||||
echo "starting reused Chatwoot frontend..."
|
||||
echo "starting GoChat frontend..."
|
||||
(cd "$CHATWOOT_DIR" && "${frontend_cmd[@]}") >"$LOG_DIR/chatwoot-vite.log" 2>&1 &
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT/app/javascript/entrypoints/dashboard.js" "Chatwoot Vite"
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT/app/javascript/entrypoints/dashboard.js" "GoChat Vite"
|
||||
|
||||
BOOT_BACKEND_RESULT="Passed boot readiness" \
|
||||
BOOT_FRONTEND_RESULT="Passed boot readiness" \
|
||||
write_report "Boot smoke passed for backend and reused Chatwoot frontend." "This run verified process boot and HTTP readiness only. B12.2 must add browser/API path assertions and update the smoke matrix."
|
||||
BOOT_FRONTEND_RESULT="Passed boot readiness" \
|
||||
write_report "Boot smoke passed for backend and GoChat frontend." "This run verified process boot and HTTP readiness only. Browser/API path assertions run in --browser-smoke."
|
||||
echo "smoke boot passed; report written to $REPORT_PATH"
|
||||
|
||||
if [[ "$KEEP_ALIVE" == "true" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user