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
81 changes: 81 additions & 0 deletions .github/workflows/ci-pkg-common-ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# vue-skuilder/.github/workflows/ci-pkg-common-ui.yml
name: CI - Common UI Package

on:
push:
branches: [main]
paths:
- 'packages/common-ui/**'
- 'packages/common/**'
- 'packages/db/**'
- '.github/workflows/ci-pkg-common-ui.yml'
pull_request:
branches: [main]
paths:
- 'packages/common-ui/**'
- 'packages/common/**'
- 'packages/db/**'
- '.github/workflows/ci-pkg-common-ui.yml'

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'yarn'

- name: Install dependencies
run: yarn install --immutable

# Build dependencies first
- name: Build common package
run: |
cd packages/common
yarn build

- name: Build db package
run: |
cd packages/db
yarn build

# Lint common-ui
- name: Lint common-ui
run: |
cd packages/common-ui
yarn lint:check

# Build common-ui
- name: Build common-ui
run: |
cd packages/common-ui
yarn build

# Run unit tests
- name: Run unit tests
run: |
cd packages/common-ui
yarn test:unit

# Run component tests
- name: Run Cypress component tests
run: |
cd packages/common-ui
yarn cypress:run --component

# Archive test artifacts if tests fail
- name: Archive Cypress screenshots and videos
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-artifacts
path: |
packages/common-ui/cypress/screenshots
packages/common-ui/cypress/videos
retention-days: 7
Comment on lines +22 to +81

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
25 changes: 25 additions & 0 deletions packages/common-ui/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// packages/common-ui/cypress.config.ts
import { defineConfig } from 'cypress';
import { defineConfig as defineViteConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
component: {
devServer: {
framework: 'vue',
bundler: 'vite',
viteConfig: defineViteConfig({
plugins: [vue()],
resolve: {
alias: {
'@': './src',
},
},
}),
},
specPattern: 'cypress/component/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/component.js',
indexHtmlFile: 'cypress/support/component-index.html', // Add this line
},
});
98 changes: 98 additions & 0 deletions packages/common-ui/cypress/component/StudySessionTimer.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// packages/common-ui/cypress/component/StudySessionTimer.cy.ts
import StudySessionTimer from '../../src/components/StudySessionTimer.vue';

describe('StudySessionTimer', () => {
it('renders correctly with default props', () => {
cy.mount(StudySessionTimer, {
props: {
timeRemaining: 180,
sessionTimeLimit: 5,
},
});

// Check for the circular progress component
cy.get('[role="progressbar"]').should('exist');

// Check for the formatted time text (visible in tooltip)
cy.get('.v-tooltip').trigger('mouseenter');
cy.contains('3:00 left!').should('exist');
});

it('changes color when time is low', () => {
cy.mount(StudySessionTimer, {
props: {
timeRemaining: 30, // 30 seconds (below 60 seconds threshold)
sessionTimeLimit: 5,
},
});

// Check for orange color class
cy.get('[role="progressbar"]')
.should('have.class', 'orange')
.or('have.class', 'orange-darken-3');

// Check for the time text
cy.get('.v-tooltip').trigger('mouseenter');
cy.contains('30 seconds left!').should('exist');
});

it('shows button on hover and emits add-time event when clicked', () => {
const onAddTime = cy.spy().as('onAddTime');

cy.mount(StudySessionTimer, {
props: {
timeRemaining: 180,
sessionTimeLimit: 5,
'onAdd-time': onAddTime,
},
});

// Button should not be visible initially
cy.get('button').should('not.be.visible');

// Button should appear on hover
cy.get('.timer-container').trigger('mouseenter');
cy.get('button').should('be.visible');

// Click should emit event
cy.get('button').click();
cy.get('@onAddTime').should('have.been.called');
});

it('does not show button when time is zero', () => {
cy.mount(StudySessionTimer, {
props: {
timeRemaining: 0,
sessionTimeLimit: 5,
},
});

// Even on hover, button should not appear
cy.get('.timer-container').trigger('mouseenter');
cy.get('button').should('not.exist');
});

it('displays correct percentage for progress indicator', () => {
cy.mount(StudySessionTimer, {
props: {
timeRemaining: 150, // 2.5 minutes
sessionTimeLimit: 5, // 5 minutes
},
});

// Progress should be 50% (2.5 minutes of 5 minutes)
cy.get('[role="progressbar"]').should('have.attr', 'aria-valuenow', '50');
});

it('displays correct percentage when under 60 seconds', () => {
cy.mount(StudySessionTimer, {
props: {
timeRemaining: 30, // 30 seconds
sessionTimeLimit: 5, // 5 minutes
},
});

// Progress should be 50% (30 seconds of 60 seconds in the final minute)
cy.get('[role="progressbar"]').should('have.attr', 'aria-valuenow', '50');
});
});
9 changes: 9 additions & 0 deletions packages/common-ui/cypress/support/component-index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare namespace Cypress {
interface Chainable<Subject> {
/**
* Custom command to mount Vue components for testing
* @example cy.mount(Component, options)
*/
mount: typeof import('cypress/vue').mount;
}
}
13 changes: 13 additions & 0 deletions packages/common-ui/cypress/support/component-index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- packages/common-ui/cypress/support/component-index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
19 changes: 19 additions & 0 deletions packages/common-ui/cypress/support/component-styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* packages/common-ui/cypress/support/component-styles.css */
body[data-cy-vuetify] {
margin: 0;
padding: 16px;
height: 100vh;
}

#app {
padding: 24px;
height: 100%;
}

/* Make sure Vuetify elements are visible in the test runner */
.v-tooltip {
position: relative !important;
opacity: 1 !important;
visibility: visible !important;
transform: none !important;
}
51 changes: 51 additions & 0 deletions packages/common-ui/cypress/support/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// packages/common-ui/cypress/support/component.js
import { mount } from 'cypress/vue';
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';

// Import Vuetify styles
import 'vuetify/styles';
import '@mdi/font/css/materialdesignicons.css';
import './component-styles.css';

// Create a more complete Vuetify instance
const vuetify = createVuetify({
components,
directives,
});

// Add mount command
Cypress.Commands.add('mount', (component, options = {}) => {
// Initialize options if not provided
if (!options.global) {
options.global = {};
}
if (!options.global.plugins) {
options.global.plugins = [];
}

// Add Vuetify to the component
options.global.plugins.push({
install(app) {
app.use(vuetify);
},
});

// Set up any global components needed

// Set up any specific Vuetify-related configurations
const el = document.createElement('div');
el.id = 'app';
document.body.appendChild(el);

// Add Vuetify CSS classes to the body
document.body.setAttribute('data-cy-vuetify', '');
document.body.classList.add('v-application');
document.body.classList.add('v-theme--light');

return mount(component, {
...options,
attachTo: '#app',
});
});
7 changes: 6 additions & 1 deletion packages/common-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"build-types": "tsc --project tsconfig.types.json",
"lint": "eslint . --fix",
"lint:check": "eslint .",
"test:unit": "vitest run"
"test:unit": "vitest run",
"cypress:open": "cypress open --component",
"cypress:run": "cypress run --component"
},
"dependencies": {
"@highlightjs/vue-plugin": "^2.1.2",
Expand All @@ -43,10 +45,13 @@
"vuetify": "^3.0.0"
},
"devDependencies": {
"@cypress/vite-dev-server": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-typescript": "^14.4.0",
"cypress": "^14.2.1",
"cypress-vite": "^1.6.0",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.0.2",
"eslint-plugin-vue": "^9.32.0",
Expand Down
Loading
Loading