Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 25 additions & 7 deletions .github/workflows/cypress-manual.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
name: Run Cypress Tests

on:
schedule:
- cron: '0 4 * * 1' # Every Monday at 4:00 AM UTC
workflow_dispatch:
inputs:
target_env:
description: Target environment for Cypress run
required: true
default: staging
type: choice
options:
- staging
- prod

jobs:
cypress-run:
Expand All @@ -19,20 +30,27 @@ jobs:
- name: Install dependencies
run: yarn install

- name: Copy production env file
if: (inputs.target_env || 'staging') == 'prod'
run: |
echo ${{ secrets.ENV_PRODUCTION }} | base64 -d > .env.prod

- name: Copy staging env file
if: (inputs.target_env || 'staging') == 'staging'
run: |
echo ${{ secrets.ENV_STAGING }} | base64 -d > .env.staging

- name: Build app
run: yarn build:staging
env:
NODE_ENV: production
APP_ENV: staging
run: npx cross-env NODE_ENV=production APP_ENV=${{ inputs.target_env || 'staging' }} next build

- name: Run Cypress tests
uses: cypress-io/github-action@v6
with:
start: yarn start:staging
start: npx cross-env NODE_ENV=production APP_ENV=${{ inputs.target_env || 'staging' }} next start -p 3005
wait-on: 'http://localhost:3005'
wait-on-timeout: 300
command: yarn cypress run
command: yarn cypress:run
env:
NODE_ENV: production
APP_ENV: staging
APP_ENV: ${{ inputs.target_env || 'staging' }}
CYPRESS_BASE_URL: http://localhost:3005
4 changes: 4 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export default defineConfig({
googleRefreshToken: process.env.GOOGLE_REFRESH_TOKEN,
googleClientId: process.env.GOOGLE_CLIENT_ID,
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
STRAPI_URL:
process.env.STRAPI_URL ||
process.env.CYPRESS_STRAPI_URL ||
'https://strapi.keepsimple.io',
},
},
});
11 changes: 7 additions & 4 deletions cypress/e2e/articles/articles-links.spec.cy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
describe('External Links from API', () => {
let routes: string[] = [];
const apiUrl =
'https://strapi.keepsimple.io/api/articles?locale=en&fields[1]=newUrl';

before(() => {
const strapiUrl =
Cypress.env('STRAPI_URL') || 'https://strapi.keepsimple.io';
const apiUrl = `${strapiUrl}/api/articles?locale=en&fields[1]=newUrl`;

cy.request(apiUrl).then(response => {
routes = response.body.data.map(
item =>
Expand All @@ -29,9 +31,10 @@ describe('External Links from API', () => {
if (
href &&
href.startsWith('http') &&
!href.includes('http:localhost:3005') &&
!href.includes('http://localhost:3005') &&
!href.includes('linkedin.com') &&
!href.includes('facebook.com')
!href.includes('facebook.com') &&
!href.includes('/uxcore')
) {
cy.request({
url: href,
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/articles/articles.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('template spec', () => {
});

it('Should show a h1', () => {
cy.checkH1('Articles');
cy.checkH1();
});

it("Should click and scroll to 'UX Core' section", () => {
Expand Down
4 changes: 1 addition & 3 deletions cypress/e2e/articles/what-is-ux-core.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ describe('template spec', () => {
});

it('Should show a h1', () => {
cy.checkH1(
'Reintroduction to UX Core - the world’s largest open-source library of nudging strategies and cognitive biases.',
);
cy.checkH1();
});

it('verifies all image src URLs are valid', () => {
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/company-management/company-management.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('template spec', () => {
});

it('Should show a h1', () => {
cy.checkH1('Pyramid of Operational Needs');
cy.checkH1();
});

it('Should start playing a music', () => {
Expand Down
6 changes: 3 additions & 3 deletions cypress/e2e/landing-page/landing-page.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ describe('template spec', () => {
cy.visit(`${Cypress.config().baseUrl}`);
});

it('should show a h1', () => {
cy.checkH1('Wolf Alexanyan');
it('Should show a h1', () => {
cy.checkH1();
});

it('should check external links and their accessibility', () => {
Expand Down Expand Up @@ -33,6 +33,6 @@ describe('template spec', () => {
.should('exist')
.click();
cy.url().should('include', '/ru');
cy.checkH1('Вольф Алексанян');
cy.checkH1();
});
});
65 changes: 65 additions & 0 deletions cypress/e2e/longevity-protocol/about-project.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export {};
const PAGE = '/tools/longevity-protocol/about-project';
const DEFAULT_DNA_SRC = '/keepsimple_/assets/longevity/dna/default.mp4';

describe('About Project – /tools/longevity-protocol/about-project', () => {
beforeEach(() => {
cy.viewport(1920, 900);
cy.visit(PAGE);
});

// 1. H1 existence
it('has a visible H1', () => {
cy.checkH1();
});

// 2. DNA canvas: default.mp4 is present, autoplaying and looping
it('renders the default DNA video with autoplay and loop', () => {
cy.get(`video[src="${DEFAULT_DNA_SRC}"]`)
.should('exist')
.should($video => {
expect($video).to.have.attr('autoplay');
expect($video).to.have.attr('loop');
});
});

// 3. All images load without undefined or broken src paths
it('has no images with undefined or broken src paths', () => {
cy.get('img').each($img => {
const src = $img.attr('src');

expect(src, 'img src should be defined').to.not.be.undefined;
expect(
src,
`img src should not contain "undefined": ${src}`,
).to.not.include('undefined');

if (src && src.startsWith('/')) {
cy.request({ url: src, failOnStatusCode: false }).then(res => {
expect(res.status, `Image returned ${res.status}: ${src}`).to.be.lt(
400,
);
});
}
});
});

// 4. Basic stats section contains all 5 stats with non-empty values
it('shows all basic stats with non-empty values', () => {
cy.get('[data-cy="basic-stats"]').should('exist');
cy.get('[data-cy="stat-item"]').should('have.length', 5);

cy.get('[data-cy="stat-value"]').each($span => {
const text = $span.text().trim();
expect(text, 'Stat value should not be empty').to.not.be.empty;
expect(text, 'Stat value should not contain "undefined"').to.not.include(
'undefined',
);
});
});

// 5. All internal and external links are valid (reuses checkPageLinks command)
it('has no broken internal or external links', () => {
cy.checkPageLinks();
});
});
162 changes: 162 additions & 0 deletions cypress/e2e/longevity-protocol/diet.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
const PAGE = '/tools/longevity-protocol/habits/diet';

describe('Diet – /tools/longevity-protocol/habits/diet', () => {
describe('Desktop (1920x900)', () => {
beforeEach(() => {
cy.viewport(1920, 900);
cy.visit(PAGE);
});

// 1. H1 existence
it('has a visible H1', () => {
cy.checkH1();
});

// 2. Japanese text existence
it('has Japanese text rendered', () => {
cy.checkJapaneseText();
});

// 3. WhatToEatOrAvoid – click second selectable item, checkbox state updates
it('updates checkbox state when clicking a WhatToEatOrAvoid item', () => {
// Items with role="checkbox" only exist in the "what to eat" section
cy.scrollTo(0, 1600);
cy.get('[data-cy="diet-checkbox"]').eq(0).scrollIntoView();

// First item starts selected – checkmark is present
cy.get('[data-cy="diet-checkbox"]')
.eq(0)
.find('[data-cy="diet-checkmark"]')
.should('exist');

// Click the second selectable item
cy.get('[data-cy="diet-checkbox"]')
.eq(1)
.closest('[data-cy="what-to-eat-or-avoid"]')
.click({ force: true });

// Second item is now checked
cy.get('[data-cy="diet-checkbox"]')
.eq(1)
.find('[data-cy="diet-checkmark"]')
.should('exist');

// First item is now unchecked
cy.get('[data-cy="diet-checkbox"]')
.eq(0)
.find('[data-cy="diet-checkmark"]')
.should('not.exist');
});

// 4. DietResults – click second item → active states update + YourDiet data changes
it('activates second DietResults item and updates YourDiet data', () => {
// Scroll to the DietResults component
cy.get('[data-cy="diet-results-item"]').first().scrollIntoView();
cy.wait(300);

// Dismiss the cookie banner so it does not cover the result items
cy.get('[data-cy="cookie-box-accept"]').click();

// Initial state: first item (id=1) is active, second is not
cy.get('[data-cy="diet-results-item"]')
.eq(0)
.should('have.attr', 'data-active', 'true');
cy.get('[data-cy="diet-results-item"]')
.eq(1)
.should('have.attr', 'data-active', 'false');

// Capture the current YourDiet selected id before the change
cy.get('[data-cy="your-diet"]')
.invoke('attr', 'data-selected-id')
.then(idBefore => {
// Click the inner img of the second item; the img click bubbles to the
// parent div's onClick and avoids any ::after overlay that may block the hit
cy.get('[data-cy="diet-results-item"]')
.eq(1)
.find('img')
.click({ force: true });

cy.wait(300);

// Active states have flipped
cy.get('[data-cy="diet-results-item"]')
.eq(1)
.should('have.attr', 'data-active', 'true');
cy.get('[data-cy="diet-results-item"]')
.eq(0)
.should('have.attr', 'data-active', 'false');

// YourDiet animation was triggered
cy.get('[data-cy="your-diet"]').should(
'have.attr',
'data-active',
'true',
);

// YourDiet is now displaying a different diet entry (data-selected-id changed)
cy.get('[data-cy="your-diet"]')
.invoke('attr', 'data-selected-id')
.should('not.equal', idBefore);
});
});
// 5. All internal and external links are valid (reuses shared checkPageLinks command)
it('has no broken internal or external links', () => {
cy.checkPageLinks();
});

// 6. All images load – no undefined in src paths
it('has no images with undefined or broken src paths', () => {
cy.get('img').each($img => {
const src = $img.attr('src');

expect(src, 'img src should be defined').to.not.be.undefined;
expect(
src,
`img src should not contain "undefined": ${src}`,
).to.not.include('undefined');

if (src && src.startsWith('/')) {
cy.request({ url: src, failOnStatusCode: false }).then(res => {
expect(res.status, `Image returned ${res.status}: ${src}`).to.be.lt(
400,
);
});
}
});
});
});

describe('Mobile (390x844)', () => {
beforeEach(() => {
cy.viewport(390, 844);
cy.visit(PAGE);
});

// Mobile modal – tapping the heart image in WhatToEatOrAvoid opens the
// AboutTheProduct modal; closing via the modal close icon dismisses it
it('opens and closes the mobile AboutTheProduct modal on heart image tap', () => {
// Dismiss the cookie banner before any interaction
cy.get('[data-cy="cookie-box-accept"]').click();

// Scroll to the first WhatToEatOrAvoid card that has a heart trigger
cy.get('[data-cy="heart-trigger"]').first().scrollIntoView();

// Tap the heart image – on mobile this opens the modal instead of a tooltip
cy.get('[data-cy="heart-trigger"]')
.first()
.find('img')
.click({ force: true });

// AboutTheProduct content is now visible inside the portal modal
cy.get('[data-cy="about-product"]').should('be.visible');

// Close via the modal close icon
cy.get('[data-cy="modal-close-icon"]').click();

// Modal and its content are gone
cy.get('[data-cy="about-product"]').should('not.exist');
});
});
});

export {};
Loading
Loading