Skip to content

Commit 36ac599

Browse files
authored
ui tweaks (#643)
- **default drawer to `open`** - **comments** - **remove unused login check** - **remove prior timer widget** - **improve 'hiding' of timer behind navbar** - **fix inlined css comment...** - **[common-ui] add cypress component testing infra** - **refactor: extract StudySessionTimer from Study** - **cypress updates to enable vuetify styling** - **add test for StudySessionTimer** - **lockfile** - **[common-ui] add ci**
2 parents bae9342 + 9de6014 commit 36ac599

File tree

15 files changed

+747
-103
lines changed

15 files changed

+747
-103
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# vue-skuilder/.github/workflows/ci-pkg-common-ui.yml
2+
name: CI - Common UI Package
3+
4+
on:
5+
push:
6+
branches: [main]
7+
paths:
8+
- 'packages/common-ui/**'
9+
- 'packages/common/**'
10+
- 'packages/db/**'
11+
- '.github/workflows/ci-pkg-common-ui.yml'
12+
pull_request:
13+
branches: [main]
14+
paths:
15+
- 'packages/common-ui/**'
16+
- 'packages/common/**'
17+
- 'packages/db/**'
18+
- '.github/workflows/ci-pkg-common-ui.yml'
19+
20+
jobs:
21+
build-and-test:
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- name: Checkout repository
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: 18
32+
cache: 'yarn'
33+
34+
- name: Install dependencies
35+
run: yarn install --immutable
36+
37+
# Build dependencies first
38+
- name: Build common package
39+
run: |
40+
cd packages/common
41+
yarn build
42+
43+
- name: Build db package
44+
run: |
45+
cd packages/db
46+
yarn build
47+
48+
# Lint common-ui
49+
- name: Lint common-ui
50+
run: |
51+
cd packages/common-ui
52+
yarn lint:check
53+
54+
# Build common-ui
55+
- name: Build common-ui
56+
run: |
57+
cd packages/common-ui
58+
yarn build
59+
60+
# Run unit tests
61+
- name: Run unit tests
62+
run: |
63+
cd packages/common-ui
64+
yarn test:unit
65+
66+
# Run component tests
67+
- name: Run Cypress component tests
68+
run: |
69+
cd packages/common-ui
70+
yarn cypress:run --component
71+
72+
# Archive test artifacts if tests fail
73+
- name: Archive Cypress screenshots and videos
74+
uses: actions/upload-artifact@v4
75+
if: failure()
76+
with:
77+
name: cypress-artifacts
78+
path: |
79+
packages/common-ui/cypress/screenshots
80+
packages/common-ui/cypress/videos
81+
retention-days: 7
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// packages/common-ui/cypress.config.ts
2+
import { defineConfig } from 'cypress';
3+
import { defineConfig as defineViteConfig } from 'vite';
4+
import vue from '@vitejs/plugin-vue';
5+
import { resolve } from 'path';
6+
7+
export default defineConfig({
8+
component: {
9+
devServer: {
10+
framework: 'vue',
11+
bundler: 'vite',
12+
viteConfig: defineViteConfig({
13+
plugins: [vue()],
14+
resolve: {
15+
alias: {
16+
'@': './src',
17+
},
18+
},
19+
}),
20+
},
21+
specPattern: 'cypress/component/**/*.cy.{js,jsx,ts,tsx}',
22+
supportFile: 'cypress/support/component.js',
23+
indexHtmlFile: 'cypress/support/component-index.html', // Add this line
24+
},
25+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// packages/common-ui/cypress/component/StudySessionTimer.cy.ts
2+
import StudySessionTimer from '../../src/components/StudySessionTimer.vue';
3+
4+
describe('StudySessionTimer', () => {
5+
it('renders correctly with default props', () => {
6+
cy.mount(StudySessionTimer, {
7+
props: {
8+
timeRemaining: 180,
9+
sessionTimeLimit: 5,
10+
},
11+
});
12+
13+
// Check for the circular progress component
14+
cy.get('[role="progressbar"]').should('exist');
15+
16+
// Check for the formatted time text (visible in tooltip)
17+
cy.get('.v-tooltip').trigger('mouseenter');
18+
cy.contains('3:00 left!').should('exist');
19+
});
20+
21+
it('changes color when time is low', () => {
22+
cy.mount(StudySessionTimer, {
23+
props: {
24+
timeRemaining: 30, // 30 seconds (below 60 seconds threshold)
25+
sessionTimeLimit: 5,
26+
},
27+
});
28+
29+
// Check for orange color class
30+
cy.get('[role="progressbar"]')
31+
.should('have.class', 'orange')
32+
.or('have.class', 'orange-darken-3');
33+
34+
// Check for the time text
35+
cy.get('.v-tooltip').trigger('mouseenter');
36+
cy.contains('30 seconds left!').should('exist');
37+
});
38+
39+
it('shows button on hover and emits add-time event when clicked', () => {
40+
const onAddTime = cy.spy().as('onAddTime');
41+
42+
cy.mount(StudySessionTimer, {
43+
props: {
44+
timeRemaining: 180,
45+
sessionTimeLimit: 5,
46+
'onAdd-time': onAddTime,
47+
},
48+
});
49+
50+
// Button should not be visible initially
51+
cy.get('button').should('not.be.visible');
52+
53+
// Button should appear on hover
54+
cy.get('.timer-container').trigger('mouseenter');
55+
cy.get('button').should('be.visible');
56+
57+
// Click should emit event
58+
cy.get('button').click();
59+
cy.get('@onAddTime').should('have.been.called');
60+
});
61+
62+
it('does not show button when time is zero', () => {
63+
cy.mount(StudySessionTimer, {
64+
props: {
65+
timeRemaining: 0,
66+
sessionTimeLimit: 5,
67+
},
68+
});
69+
70+
// Even on hover, button should not appear
71+
cy.get('.timer-container').trigger('mouseenter');
72+
cy.get('button').should('not.exist');
73+
});
74+
75+
it('displays correct percentage for progress indicator', () => {
76+
cy.mount(StudySessionTimer, {
77+
props: {
78+
timeRemaining: 150, // 2.5 minutes
79+
sessionTimeLimit: 5, // 5 minutes
80+
},
81+
});
82+
83+
// Progress should be 50% (2.5 minutes of 5 minutes)
84+
cy.get('[role="progressbar"]').should('have.attr', 'aria-valuenow', '50');
85+
});
86+
87+
it('displays correct percentage when under 60 seconds', () => {
88+
cy.mount(StudySessionTimer, {
89+
props: {
90+
timeRemaining: 30, // 30 seconds
91+
sessionTimeLimit: 5, // 5 minutes
92+
},
93+
});
94+
95+
// Progress should be 50% (30 seconds of 60 seconds in the final minute)
96+
cy.get('[role="progressbar"]').should('have.attr', 'aria-valuenow', '50');
97+
});
98+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare namespace Cypress {
2+
interface Chainable<Subject> {
3+
/**
4+
* Custom command to mount Vue components for testing
5+
* @example cy.mount(Component, options)
6+
*/
7+
mount: typeof import('cypress/vue').mount;
8+
}
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!-- packages/common-ui/cypress/support/component-index.html -->
2+
<!DOCTYPE html>
3+
<html>
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
7+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
8+
<title>Components App</title>
9+
</head>
10+
<body>
11+
<div data-cy-root></div>
12+
</body>
13+
</html>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* packages/common-ui/cypress/support/component-styles.css */
2+
body[data-cy-vuetify] {
3+
margin: 0;
4+
padding: 16px;
5+
height: 100vh;
6+
}
7+
8+
#app {
9+
padding: 24px;
10+
height: 100%;
11+
}
12+
13+
/* Make sure Vuetify elements are visible in the test runner */
14+
.v-tooltip {
15+
position: relative !important;
16+
opacity: 1 !important;
17+
visibility: visible !important;
18+
transform: none !important;
19+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// packages/common-ui/cypress/support/component.js
2+
import { mount } from 'cypress/vue';
3+
import { createVuetify } from 'vuetify';
4+
import * as components from 'vuetify/components';
5+
import * as directives from 'vuetify/directives';
6+
7+
// Import Vuetify styles
8+
import 'vuetify/styles';
9+
import '@mdi/font/css/materialdesignicons.css';
10+
import './component-styles.css';
11+
12+
// Create a more complete Vuetify instance
13+
const vuetify = createVuetify({
14+
components,
15+
directives,
16+
});
17+
18+
// Add mount command
19+
Cypress.Commands.add('mount', (component, options = {}) => {
20+
// Initialize options if not provided
21+
if (!options.global) {
22+
options.global = {};
23+
}
24+
if (!options.global.plugins) {
25+
options.global.plugins = [];
26+
}
27+
28+
// Add Vuetify to the component
29+
options.global.plugins.push({
30+
install(app) {
31+
app.use(vuetify);
32+
},
33+
});
34+
35+
// Set up any global components needed
36+
37+
// Set up any specific Vuetify-related configurations
38+
const el = document.createElement('div');
39+
el.id = 'app';
40+
document.body.appendChild(el);
41+
42+
// Add Vuetify CSS classes to the body
43+
document.body.setAttribute('data-cy-vuetify', '');
44+
document.body.classList.add('v-application');
45+
document.body.classList.add('v-theme--light');
46+
47+
return mount(component, {
48+
...options,
49+
attachTo: '#app',
50+
});
51+
});

packages/common-ui/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
"build-types": "tsc --project tsconfig.types.json",
2424
"lint": "eslint . --fix",
2525
"lint:check": "eslint .",
26-
"test:unit": "vitest run"
26+
"test:unit": "vitest run",
27+
"cypress:open": "cypress open --component",
28+
"cypress:run": "cypress run --component"
2729
},
2830
"dependencies": {
2931
"@highlightjs/vue-plugin": "^2.1.2",
@@ -43,10 +45,13 @@
4345
"vuetify": "^3.0.0"
4446
},
4547
"devDependencies": {
48+
"@cypress/vite-dev-server": "^6.0.3",
4649
"@typescript-eslint/eslint-plugin": "^8.25.0",
4750
"@typescript-eslint/parser": "^8.25.0",
4851
"@vitejs/plugin-vue": "^5.2.1",
4952
"@vue/eslint-config-typescript": "^14.4.0",
53+
"cypress": "^14.2.1",
54+
"cypress-vite": "^1.6.0",
5055
"eslint": "^9.21.0",
5156
"eslint-config-prettier": "^10.0.2",
5257
"eslint-plugin-vue": "^9.32.0",

0 commit comments

Comments
 (0)