Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e7c5e22
CC-39427: Add Cypress tests for all demo AI features with dynamic fix…
vitaliiivanovspryker Jun 27, 2026
6a6e541
CC-39427: Refactor demo AI test cases to improve descriptions and ali…
vitaliiivanovspryker Jun 27, 2026
696e3ff
CC-39427:
vitaliiivanovspryker Jun 27, 2026
05be799
Upmerge cypress master (@4f00726f) into master-demo for CC-39486
vitaliiivanovspryker Jul 1, 2026
c8bc281
СС-39512: add @demo-smoke tags to AI workflows, audit logs, backoffic…
vitaliiivanovspryker Jul 2, 2026
c68fc3c
CC-39512: enable `DEMO_AI_PROVIDER` flag in Cypress tests and update …
vitaliiivanovspryker Jul 9, 2026
61690b0
CC-39512:
vitaliiivanovspryker Jul 13, 2026
5545494
СС-39512 demo-test-full: Standardize Cypress tests and improve mainta…
vitaliiivanovspryker Jul 14, 2026
b6153bd
Merge pull request #356 from spryker/test/cc-39512/demo-test-full
vitaliiivanovspryker Jul 16, 2026
4eaeff2
Upmerge cypress master (@ebf6602f) into master-demo for 2026-07-17
vitaliiivanovspryker Jul 17, 2026
cfff9c8
CC-39891: align BO Assistant tool-call assertions with CC-39878 UI
vitaliiivanovspryker Jul 30, 2026
5d56476
CC-39891: resolve demo product by SKU instead of hard-coded id_produc…
vitaliiivanovspryker Jul 30, 2026
a0fac48
Upmerge cypress master (@fcf092ca) into master-demo for CC-39940
vitaliiivanovspryker Aug 5, 2026
6e91a1f
Upmerge cypress master (@993cfa72) into master-demo for CC-40004
vitaliiivanovspryker Aug 14, 2026
af88686
Upmerge cypress master (@644cd125) into master-demo for CC-40043
vitaliiivanovspryker Aug 20, 2026
e15b089
CC-39797 Adjustment for MP payment
olhalivitchuk Aug 26, 2026
4e1d590
CC-39797 Adjustment for MP payment
olhalivitchuk Aug 26, 2026
3bf8b06
CC-39797 Adjustment for MP payment
olhalivitchuk Aug 26, 2026
b36d5c5
Upmerge cypress master (@3bf8b060) into master-demo for CC-39862
vitaliiivanovspryker Aug 31, 2026
7d1deee
style: apply prettier formatting to cost-price and smart-pim demo specs
vitaliiivanovspryker Aug 31, 2026
b0f1a7d
style: satisfy spryker-cypress ESLint convention rules in demo tests
vitaliiivanovspryker Aug 31, 2026
61a517a
Upmerge cypress master (@85cd2e6913ad2ac452ac9ce3e772485a72672bac) in…
vitaliiivanovspryker Sep 14, 2026
10e0801
Upmerge cypress master (@6831951d0076de739e35ffdc794bf58384ecd980) in…
vitaliiivanovspryker Sep 16, 2026
9d77561
Upmerge cypress master (@6a12cf97b2e9) into master-demo for CC-40717
vitaliiivanovspryker Sep 29, 2026
d2e6d8c
CC-40717: match quick-order image-to-cart error list by list--error o…
vitaliiivanovspryker Sep 29, 2026
09fb822
Upmerge cypress master (@bb737df20a8e) into master-demo for CC-40847
vitaliiivanovspryker Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ ENV_IS_SSP_ENABLED=false
E2E_BASE_HOST=yves.de.spryker.local
VIEWPORT_WIDGTH=1920
VIEWPORT_HEIGHT=1080

# Gates the @demo-full tier (cy:demo:full): set to 1 once real AI provider tokens are configured in the Back Office.
DEMO_AI_PROVIDER_ENABLED=1
1 change: 1 addition & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default defineConfig({
env: {
CI: true,
repositoryId: process.env.ENV_REPOSITORY_ID,
DEMO_AI_PROVIDER_ENABLED: process.env.DEMO_AI_PROVIDER_ENABLED === '1',
isDynamicStoreEnabled: getEnvVar('ENV_IS_DYNAMIC_STORE_ENABLED', 'SPRYKER_DYNAMIC_STORE_MODE') === 'true',
ENV_IS_SSP_ENABLED: getEnvVar('ENV_IS_SSP_ENABLED', 'ENV_IS_SSP_ENABLED') === 'true',
backofficeUrl: `${protocol}://${backofficeHost}`,
Expand Down
380 changes: 380 additions & 0 deletions cypress/e2e/demo/00-ai-configuration.cy.ts

Large diffs are not rendered by default.

112 changes: 112 additions & 0 deletions cypress/e2e/demo/ai-workflows.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { container } from '@utils';
import { UserLoginScenario } from '@scenarios/backoffice';
import { AiWorkflowsPage } from '@pages/backoffice';
import { AiWorkflowsDemoStaticFixtures } from '@interfaces/demo';

describe(
'AI Workflows - Back Office workflow runs list',
{
tags: ['@demo', '@ai-workflows', '@ai-foundation'],
},
(): void => {
const userLoginScenario = container.get(UserLoginScenario);
const aiWorkflowsPage = container.get(AiWorkflowsPage);

const EXPECTED_COLUMN_HEADERS: Array<{ dataQa: string; label: string }> = [
{ dataQa: 'spy_ai_workflow_item.id_ai_workflow_item', label: 'ID' },
{ dataQa: 'process_name', label: 'Process Name' },
{ dataQa: 'state_name', label: 'State' },
{ dataQa: 'spy_ai_workflow_item.created_at', label: 'Created At' },
{ dataQa: 'spy_ai_workflow_item.updated_at', label: 'Updated At' },
{ dataQa: 'Actions', label: 'Actions' },
];

const SORTABLE_COLUMNS: Array<string> = [
'spy_ai_workflow_item.id_ai_workflow_item',
'process_name',
'state_name',
'spy_ai_workflow_item.created_at',
'spy_ai_workflow_item.updated_at',
];

const NON_SORTABLE_COLUMNS: Array<string> = ['Actions'];

let staticFixtures: AiWorkflowsDemoStaticFixtures;

before((): void => {
staticFixtures = Cypress.env('staticFixtures');
});

beforeEach((): void => {
userLoginScenario.execute({
username: staticFixtures.rootUser.username,
password: staticFixtures.defaultPassword,
});
});

it(
'opens the Workflows page (HTTP 200) with the section heading, widget title and all six column headers',
{ tags: ['@demo-smoke'] },
(): void => {
aiWorkflowsPage.visitAiWorkflows().its('response.statusCode').should('eq', 200);

aiWorkflowsPage.getSectionTitle().should('contain.text', aiWorkflowsPage.getSectionTitleText());
aiWorkflowsPage.getWidgetTitle().should('contain.text', aiWorkflowsPage.getWidgetTitleText());

aiWorkflowsPage.getTable().should('exist');
aiWorkflowsPage.getTableHeaders().should('have.length', EXPECTED_COLUMN_HEADERS.length);
EXPECTED_COLUMN_HEADERS.forEach((column): void => {
aiWorkflowsPage.getColumnHeader(column.dataQa).should('exist').and('contain.text', column.label);
});
}
);

it(
'initializes a live DataTable (HTTP 200 JSON with the draw/recordsTotal/recordsFiltered/data shape) and marks the five data columns sortable and Actions non-sortable',
{ tags: ['@demo-smoke'] },
(): void => {
aiWorkflowsPage.visitAndAwaitTableData().then((interception): void => {
expect(interception.response?.statusCode).to.eq(200);
expect(interception.response?.headers['content-type']).to.contain('application/json');

const body = interception.response?.body;
expect(body).to.have.property('draw');
expect(body).to.have.property('recordsTotal');
expect(body).to.have.property('recordsFiltered');
expect(body).to.have.property('data');
expect(body.data).to.be.an('array');
});

aiWorkflowsPage.getTableWrapper().should('exist');
aiWorkflowsPage.getTableInfo().should('be.visible');

SORTABLE_COLUMNS.forEach((column): void => {
aiWorkflowsPage.getSortableColumnHeader(column).should('exist');
});
NON_SORTABLE_COLUMNS.forEach((column): void => {
aiWorkflowsPage.getNonSortableColumnHeader(column).should('exist');
});
}
);

it(
'the page-length control and the Created At sort header each issue a fresh table data request that returns HTTP 200',
{ tags: ['@demo-smoke'] },
(): void => {
aiWorkflowsPage.visitAndAwaitTableData();

aiWorkflowsPage.getLengthSelect().should('exist');
aiWorkflowsPage.aliasTableData('lengthChangeData');
aiWorkflowsPage.selectPageLength('50');
cy.wait('@lengthChangeData').then((interception): void => {
expect(interception.response?.statusCode).to.eq(200);
expect(interception.request.url).to.contain('length=50');
});

aiWorkflowsPage.aliasTableData('sortChangeData');
aiWorkflowsPage.getSortableColumnHeader('spy_ai_workflow_item.created_at').click();
cy.wait('@sortChangeData').its('response.statusCode').should('eq', 200);
}
);
}
);
203 changes: 203 additions & 0 deletions cypress/e2e/demo/audit-logs.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { container, skipUnlessAiProviderEnabled } from '@utils';
import { UserLoginScenario } from '@scenarios/backoffice';
import { AuditLogsPage, SmartPimPage } from '@pages/backoffice';
import { AuditLogsDemoStaticFixtures } from '@interfaces/demo';

describe(
'Audit Logs - Back Office AI interaction log',
{
tags: ['@demo', '@audit-logs', '@ai-foundation'],
},
(): void => {
const userLoginScenario = container.get(UserLoginScenario);
const auditLogsPage = container.get(AuditLogsPage);
const smartPimPage = container.get(SmartPimPage);

const EXPECTED_COLUMN_HEADERS: Array<{ dataQa: string; label: string }> = [
{ dataQa: 'spy_ai_interaction_log.prompt', label: 'Prompt' },
{ dataQa: 'spy_ai_interaction_log.conversation_reference', label: 'Conversation' },
{ dataQa: 'spy_ai_interaction_log.provider', label: 'Provider' },
{ dataQa: 'spy_ai_interaction_log.model', label: 'Model' },
{ dataQa: 'total_tokens', label: 'Total Tokens' },
{ dataQa: 'estimated_cost', label: 'Estimated cost' },
{ dataQa: 'spy_ai_interaction_log.configuration_name', label: 'Configuration' },
{ dataQa: 'spy_ai_interaction_log.is_successful', label: 'Status' },
{ dataQa: 'spy_ai_interaction_log.inference_time_ms', label: 'Inference (ms)' },
{ dataQa: 'spy_ai_interaction_log.created_at', label: 'Created At' },
];

const EXPECTED_STATS_CARDS: Array<string> = [
'Total Interactions',
'Total Tokens',
'Total estimated cost',
'Success Rate',
'Avg Inference Time',
];

const SORTABLE_COLUMNS: Array<string> = [
'spy_ai_interaction_log.configuration_name',
'spy_ai_interaction_log.is_successful',
'spy_ai_interaction_log.created_at',
];

const NON_SORTABLE_COLUMNS: Array<string> = [
'spy_ai_interaction_log.prompt',
'spy_ai_interaction_log.conversation_reference',
'spy_ai_interaction_log.provider',
'spy_ai_interaction_log.model',
'total_tokens',
'estimated_cost',
'spy_ai_interaction_log.inference_time_ms',
];

let staticFixtures: AuditLogsDemoStaticFixtures;

before((): void => {
staticFixtures = Cypress.env('staticFixtures');
});

beforeEach((): void => {
userLoginScenario.execute({
username: staticFixtures.rootUser.username,
password: staticFixtures.defaultPassword,
});
});

it(
'page opens (HTTP 200) with the section heading, all ten column headers and the five summary stats cards',
{ tags: ['@demo-smoke'] },
(): void => {
auditLogsPage.visitAuditLogs().its('response.statusCode').should('eq', 200);

auditLogsPage.getSectionTitle().should('contain.text', auditLogsPage.getSectionTitleText());

auditLogsPage.getTable().should('exist');
auditLogsPage.getTableHeaders().should('have.length', EXPECTED_COLUMN_HEADERS.length);
EXPECTED_COLUMN_HEADERS.forEach((column): void => {
auditLogsPage.getColumnHeader(column.dataQa).should('exist').and('contain.text', column.label);
});

auditLogsPage.getStatsCards().should('be.visible');
auditLogsPage.getStatsCardTitles().should('have.length.at.least', EXPECTED_STATS_CARDS.length);
EXPECTED_STATS_CARDS.forEach((cardTitle): void => {
auditLogsPage.getStatsCards().should('contain.text', cardTitle);
});
}
);

it(
'initializes a live DataTable (HTTP 200 JSON with the recordsTotal/recordsFiltered/data shape) and loads the stats partial over AJAX, and marks Configuration/Status/Created At sortable and the rest non-sortable',
{ tags: ['@demo-smoke'] },
(): void => {
auditLogsPage.visitAndAwaitTableData().then((interception): void => {
expect(interception.response?.statusCode).to.eq(200);
expect(interception.response?.headers['content-type']).to.contain('application/json');

const body = interception.response?.body;
expect(body).to.have.property('draw');
expect(body).to.have.property('recordsTotal');
expect(body).to.have.property('recordsFiltered');
expect(body).to.have.property('data');
expect(body.data).to.be.an('array');
});

auditLogsPage.getTableWrapper().should('exist');
auditLogsPage.getTableInfo().should('be.visible');

auditLogsPage.waitForStats().its('response.statusCode').should('eq', 200);
auditLogsPage.getStatsCards().should('contain.text', auditLogsPage.getTotalInteractionsCardText());

SORTABLE_COLUMNS.forEach((column): void => {
auditLogsPage.getSortableColumnHeader(column).should('exist');
});
NON_SORTABLE_COLUMNS.forEach((column): void => {
auditLogsPage.getNonSortableColumnHeader(column).should('exist');
});
}
);

it(
'the page-length control and the Created At sort header each issue a fresh table data request that returns HTTP 200',
{ tags: ['@demo-smoke'] },
(): void => {
auditLogsPage.visitAndAwaitTableData();

auditLogsPage.getLengthSelect().should('exist');
auditLogsPage.aliasTableData('lengthChangeData');
auditLogsPage.selectPageLength('50');
cy.wait('@lengthChangeData').then((interception): void => {
expect(interception.response?.statusCode).to.eq(200);
expect(interception.request.url).to.contain('length=50');
});

auditLogsPage.aliasTableData('sortChangeData');
auditLogsPage.getSortableColumnHeader('spy_ai_interaction_log.created_at').click();
cy.wait('@sortChangeData').its('response.statusCode').should('eq', 200);
}
);

describe('real AI audit-log data (full, requires provider token)', { tags: ['@demo-full'] }, (): void => {
const REAL_FLOW_TIMEOUT = 15000;

it(
'captures a self-seeded AI interaction as an audit-log row with populated provider/model/tokens/cost/status/inference fields',
{ tags: ['@demo-full'] },
function (): void {
skipUnlessAiProviderEnabled(this);

auditLogsPage.fetchTableData().then((baseline): void => {
const baselineTotal = baseline.recordsTotal;
const baselineCreatedAt = auditLogsPage.getRowCreatedAt(baseline.rows[0]);

cy.intercept('POST', '**/ai-commerce/content-improver').as('contentImprover');

// Resolve by SKU: id_product_abstract is import-order dependent, not stable across envs.
smartPimPage
.resolveProductAbstractIdBySku(staticFixtures.product.sku)
.then((id) => smartPimPage.visitProductEdit(id));
smartPimPage.openAllActionsPopover();
smartPimPage.shouldBeOpenPopover(smartPimPage.getAllActionsPopover());
smartPimPage.clickImproveContent();

cy.wait('@contentImprover', { timeout: REAL_FLOW_TIMEOUT })
.its('response.statusCode')
.should('be.within', 200, 299);

smartPimPage.shouldBeOpenPopover(smartPimPage.getResponsePopover());
smartPimPage.getResponseField().should('not.have.value', '');

auditLogsPage.fetchTableData().then((after): void => {
const newRowCount = after.recordsTotal - baselineTotal;

expect(newRowCount, 'at least one interaction was created after the baseline').to.be.at.least(1);

const row = after.rows.slice(0, newRowCount)[0];
expect(
auditLogsPage.getRowCreatedAt(row) >= baselineCreatedAt,
'the new row is at least as recent as the baseline newest row'
).to.eq(true);

const toNumber = (value: unknown): number => parseInt(String(value ?? '').replace(/[^\d-]/g, ''), 10);

const provider = String(row['spy_ai_interaction_log.provider'] ?? '');
const model = String(row['spy_ai_interaction_log.model'] ?? '');
const totalTokens = toNumber(row.total_tokens);
const estimatedCost = String(row.estimated_cost ?? '');
const status = String(row['spy_ai_interaction_log.is_successful'] ?? '');
const inferenceMs = toNumber(row['spy_ai_interaction_log.inference_time_ms']);

expect(provider.trim(), 'provider is captured').to.not.be.empty;
expect(model.trim(), 'model is captured').to.not.be.empty;
expect(totalTokens, 'total_tokens parses to a positive number').to.be.greaterThan(0);
expect(estimatedCost.trim(), 'estimated_cost is present (formatted string)').to.not.be.empty;
expect(status, 'status reflects a successful interaction').to.contain(
auditLogsPage.getSuccessStatusText()
);
expect(inferenceMs, 'inference_time_ms parses to a non-negative number').to.be.at.least(0);
});
});
}
);
});
}
);
Loading
Loading