mirror of
https://github.com/Lissy93/dashy.git
synced 2026-03-23 21:22:57 +01:00
👷 Adds CI for running tests
This commit is contained in:
parent
099e6e4c00
commit
7fb485ecf2
11 changed files with 1913 additions and 721 deletions
76
.github/workflows/pr-quality-check.yml
vendored
76
.github/workflows/pr-quality-check.yml
vendored
|
|
@ -5,16 +5,12 @@ on:
|
|||
branches: ['master', 'develop']
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
- '.github/PULL_REQUEST_TEMPLATE/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Job 1: Lint the code
|
||||
lint:
|
||||
name: 📝 Lint Code
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -26,15 +22,14 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: npm ci
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: 🔍 Run ESLint
|
||||
run: npm run lint
|
||||
run: yarn lint
|
||||
|
||||
# Job 2: Run tests
|
||||
test:
|
||||
name: 🧪 Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -46,23 +41,14 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: npm ci
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: 🧪 Run Tests
|
||||
run: npm test
|
||||
run: yarn test
|
||||
|
||||
- name: 📊 Upload Coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
retention-days: 7
|
||||
|
||||
# Job 3: Build the app
|
||||
build:
|
||||
name: 🏗️ Build Application
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -74,13 +60,13 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: npm ci
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: 🏗️ Build Project
|
||||
run: npm run build
|
||||
run: yarn build
|
||||
env:
|
||||
NODE_OPTIONS: --openssl-legacy-provider
|
||||
|
||||
|
|
@ -96,10 +82,22 @@ jobs:
|
|||
fi
|
||||
echo "✅ Build successful"
|
||||
|
||||
# Job 4: Security audit
|
||||
docker-smoke:
|
||||
name: 🐳 Docker Smoke Test
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: 🛎️ Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🐳 Build & Test Docker Image
|
||||
run: sh tests/docker-smoke-test.sh
|
||||
timeout-minutes: 10
|
||||
|
||||
security:
|
||||
name: 🔒 Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: 🛎️ Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
|
@ -108,34 +106,10 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: npm ci
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: 🔒 Run Security Audit
|
||||
run: npm audit --production --audit-level=high
|
||||
continue-on-error: true
|
||||
|
||||
# Final job that depends on all others
|
||||
quality-gate:
|
||||
name: ✅ Quality Gate
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test, build]
|
||||
if: always()
|
||||
steps:
|
||||
- name: 🎯 Check All Jobs Passed
|
||||
run: |
|
||||
if [ "${{ needs.lint.result }}" != "success" ]; then
|
||||
echo "❌ Lint job failed"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${{ needs.test.result }}" != "success" ]; then
|
||||
echo "❌ Test job failed"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${{ needs.build.result }}" != "success" ]; then
|
||||
echo "❌ Build job failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ All quality checks passed!"
|
||||
run: yarn audit --level high
|
||||
|
|
|
|||
16
package.json
16
package.json
|
|
@ -55,6 +55,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.17.10",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"@vue/cli-plugin-babel": "^4.5.15",
|
||||
"@vue/cli-plugin-eslint": "^4.5.15",
|
||||
"@vue/cli-plugin-pwa": "^4.5.15",
|
||||
|
|
@ -62,7 +63,6 @@
|
|||
"@vue/cli-service": "^4.5.19",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"@vue/test-utils": "^1.3.6",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"copy-webpack-plugin": "6.4.0",
|
||||
"eslint": "^6.8.0",
|
||||
|
|
@ -73,6 +73,7 @@
|
|||
"sass": "^1.38.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"typescript": "^5.4.4",
|
||||
"vite-plugin-vue2": "^2.0.3",
|
||||
"vitest": "^1.6.0",
|
||||
"vue-cli-plugin-yaml": "^1.0.2",
|
||||
"vue-svg-loader": "^0.16.0",
|
||||
|
|
@ -99,7 +100,18 @@
|
|||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["tests/**", "vitest.config.js"],
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"no-undef": "off",
|
||||
"global-require": "off",
|
||||
"no-unused-vars": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
|
|
|
|||
|
|
@ -1,79 +1,726 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VTooltip from 'v-tooltip';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VTooltip);
|
||||
|
||||
// Mock the Item component dependencies
|
||||
const mockItem = {
|
||||
id: 'test-1',
|
||||
title: 'Test Item',
|
||||
description: 'Test Description',
|
||||
url: 'https://example.com',
|
||||
icon: 'fas fa-rocket',
|
||||
};
|
||||
|
||||
describe('Item Component Structure', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new Vuex.Store({
|
||||
state: {
|
||||
config: {
|
||||
appConfig: {},
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
appConfig: (state) => state.config.appConfig,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('has required props', () => {
|
||||
// This test verifies the item data structure
|
||||
expect(mockItem).toHaveProperty('id');
|
||||
expect(mockItem).toHaveProperty('title');
|
||||
expect(mockItem).toHaveProperty('url');
|
||||
});
|
||||
|
||||
it('url is valid format', () => {
|
||||
expect(mockItem.url).toMatch(/^https?:\/\//);
|
||||
});
|
||||
|
||||
it('contains expected properties', () => {
|
||||
expect(mockItem.title).toBe('Test Item');
|
||||
expect(mockItem.description).toBe('Test Description');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Item Data Validation', () => {
|
||||
it('validates required item fields', () => {
|
||||
const validItem = {
|
||||
title: 'Valid Item',
|
||||
url: 'https://valid.com',
|
||||
};
|
||||
|
||||
expect(validItem.title).toBeTruthy();
|
||||
expect(validItem.url).toBeTruthy();
|
||||
expect(validItem.url).toMatch(/^https?:\/\//);
|
||||
});
|
||||
|
||||
it('rejects items without title', () => {
|
||||
const invalidItem = {
|
||||
url: 'https://example.com',
|
||||
};
|
||||
|
||||
expect(invalidItem.title).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects items without url', () => {
|
||||
const invalidItem = {
|
||||
title: 'Test',
|
||||
};
|
||||
|
||||
expect(invalidItem.url).toBeUndefined();
|
||||
});
|
||||
});
|
||||
import {
|
||||
describe, it, expect, beforeEach, afterEach, vi,
|
||||
} from 'vitest';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import Item from '@/components/LinkItems/Item.vue';
|
||||
import router from '@/router';
|
||||
|
||||
vi.mock('axios', () => ({ default: { get: vi.fn(() => Promise.resolve({ data: {} })) } }));
|
||||
vi.mock('@/router', () => ({ default: { push: vi.fn() } }));
|
||||
vi.mock('@/utils/ErrorHandler', () => ({ default: vi.fn() }));
|
||||
vi.mock('@/assets/interface-icons/interactive-editor-edit-mode.svg', () => ({
|
||||
default: { template: '<span />' },
|
||||
}));
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.directive('tooltip', {});
|
||||
localVue.directive('longPress', {});
|
||||
localVue.directive('clickOutside', {});
|
||||
|
||||
/** Factory — accepts overrides for item, props, appConfig, storeState, etc. */
|
||||
function mountItem(overrides = {}) {
|
||||
const item = overrides.item || {
|
||||
id: 'test-1',
|
||||
title: 'Test Item',
|
||||
description: 'A test description',
|
||||
url: 'https://example.com',
|
||||
icon: 'fas fa-rocket',
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
SET_MODAL_OPEN: vi.fn(),
|
||||
REMOVE_ITEM: vi.fn(),
|
||||
...(overrides.mutations || {}),
|
||||
};
|
||||
|
||||
const storeState = {
|
||||
editMode: false,
|
||||
config: { appConfig: overrides.appConfig || {} },
|
||||
...(overrides.storeState || {}),
|
||||
};
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: storeState,
|
||||
getters: {
|
||||
appConfig: (state) => state.config.appConfig,
|
||||
iconSize: () => overrides.iconSize || 'medium',
|
||||
getParentSectionOfItem: () => () => overrides.parentSection || { name: 'Default' },
|
||||
},
|
||||
mutations,
|
||||
});
|
||||
|
||||
const wrapper = shallowMount(Item, {
|
||||
localVue,
|
||||
store,
|
||||
propsData: { item, ...(overrides.props || {}) },
|
||||
mocks: {
|
||||
$modal: { show: vi.fn(), hide: vi.fn() },
|
||||
$toasted: { show: vi.fn() },
|
||||
$t: (key) => key,
|
||||
...(overrides.mocks || {}),
|
||||
},
|
||||
stubs: {
|
||||
Icon: true,
|
||||
ItemOpenMethodIcon: true,
|
||||
StatusIndicator: true,
|
||||
ContextMenu: true,
|
||||
MoveItemTo: true,
|
||||
EditItem: true,
|
||||
EditModeIcon: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { wrapper, store, mutations };
|
||||
}
|
||||
|
||||
let openSpy;
|
||||
let clipboardSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
openSpy = vi.spyOn(window, 'open').mockImplementation(() => {});
|
||||
clipboardSpy = vi.fn(() => Promise.resolve());
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: clipboardSpy },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
localStorage.getItem.mockReset();
|
||||
localStorage.setItem.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Computed: itemIcon', () => {
|
||||
it('returns item.icon when set', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', icon: 'my-icon',
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.itemIcon).toBe('my-icon');
|
||||
});
|
||||
|
||||
it('falls back to appConfig.defaultIcon', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: { id: '1', title: 'X', url: '#' },
|
||||
appConfig: { defaultIcon: 'default-icon' },
|
||||
});
|
||||
expect(wrapper.vm.itemIcon).toBe('default-icon');
|
||||
});
|
||||
|
||||
it('returns falsy when neither is set', () => {
|
||||
const { wrapper } = mountItem({ item: { id: '1', title: 'X', url: '#' } });
|
||||
expect(wrapper.vm.itemIcon).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed: size', () => {
|
||||
it('returns valid itemSize prop', () => {
|
||||
const { wrapper } = mountItem({ props: { itemSize: 'large' } });
|
||||
expect(wrapper.vm.size).toBe('large');
|
||||
});
|
||||
|
||||
it('ignores invalid itemSize and falls back to store', () => {
|
||||
const { wrapper } = mountItem({ props: { itemSize: 'bogus' }, iconSize: 'small' });
|
||||
expect(wrapper.vm.size).toBe('small');
|
||||
});
|
||||
|
||||
it('falls back to store iconSize getter', () => {
|
||||
const { wrapper } = mountItem({ iconSize: 'small' });
|
||||
expect(wrapper.vm.size).toBe('small');
|
||||
});
|
||||
|
||||
it('defaults to medium', () => {
|
||||
const { wrapper } = mountItem();
|
||||
expect(wrapper.vm.size).toBe('medium');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed: makeColumnCount', () => {
|
||||
it.each([
|
||||
[300, 1], [400, 2], [600, 3], [800, 4], [1100, 5], [1500, 0],
|
||||
])('sectionWidth %i → %i columns', (width, expected) => {
|
||||
const { wrapper } = mountItem({ props: { sectionWidth: width } });
|
||||
expect(wrapper.vm.makeColumnCount).toBe(expected);
|
||||
});
|
||||
|
||||
it('uses sectionDisplayData.itemCountX when set', () => {
|
||||
const { wrapper } = mountItem({
|
||||
props: { sectionWidth: 300, sectionDisplayData: { itemCountX: 7 } },
|
||||
});
|
||||
expect(wrapper.vm.makeColumnCount).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed: makeClassList', () => {
|
||||
it('includes size-{size}', () => {
|
||||
const { wrapper } = mountItem({ props: { itemSize: 'small' } });
|
||||
expect(wrapper.vm.makeClassList).toContain('size-small');
|
||||
});
|
||||
|
||||
it('includes "short" when no icon', () => {
|
||||
const { wrapper } = mountItem({ item: { id: '1', title: 'X', url: '#' } });
|
||||
expect(wrapper.vm.makeClassList).toContain('short');
|
||||
});
|
||||
|
||||
it('includes "add-new" when isAddNew', () => {
|
||||
const { wrapper } = mountItem({ props: { isAddNew: true } });
|
||||
expect(wrapper.vm.makeClassList).toContain('add-new');
|
||||
});
|
||||
|
||||
it('includes "is-edit-mode" when editMode is true', () => {
|
||||
const { wrapper } = mountItem({ storeState: { editMode: true, config: { appConfig: {} } } });
|
||||
expect(wrapper.vm.makeClassList).toContain('is-edit-mode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed: enableStatusCheck', () => {
|
||||
it('item.statusCheck boolean overrides appConfig', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', statusCheck: false,
|
||||
},
|
||||
appConfig: { statusCheck: true },
|
||||
});
|
||||
expect(wrapper.vm.enableStatusCheck).toBe(false);
|
||||
});
|
||||
|
||||
it('falls back to appConfig.statusCheck', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: { id: '1', title: 'X', url: '#' },
|
||||
appConfig: { statusCheck: true },
|
||||
});
|
||||
expect(wrapper.vm.enableStatusCheck).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults to false', () => {
|
||||
const { wrapper } = mountItem({ item: { id: '1', title: 'X', url: '#' } });
|
||||
expect(wrapper.vm.enableStatusCheck).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed: statusCheckInterval', () => {
|
||||
it('reads from item', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', statusCheckInterval: 30,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.statusCheckInterval).toBe(30);
|
||||
});
|
||||
|
||||
it('falls back to appConfig', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: { id: '1', title: 'X', url: '#' },
|
||||
appConfig: { statusCheckInterval: 15 },
|
||||
});
|
||||
expect(wrapper.vm.statusCheckInterval).toBe(15);
|
||||
});
|
||||
|
||||
it('clamps to max 60', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', statusCheckInterval: 120,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.statusCheckInterval).toBe(60);
|
||||
});
|
||||
|
||||
it('clamps values less than 1 to 0', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', statusCheckInterval: 0.5,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.statusCheckInterval).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed: accumulatedTarget', () => {
|
||||
it('uses item.target first', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', target: 'workspace',
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.accumulatedTarget).toBe('workspace');
|
||||
});
|
||||
|
||||
it('falls back to appConfig.defaultOpeningMethod', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: { id: '1', title: 'X', url: '#' },
|
||||
appConfig: { defaultOpeningMethod: 'sametab' },
|
||||
});
|
||||
expect(wrapper.vm.accumulatedTarget).toBe('sametab');
|
||||
});
|
||||
|
||||
it('defaults to "newtab"', () => {
|
||||
const { wrapper } = mountItem({ item: { id: '1', title: 'X', url: '#' } });
|
||||
expect(wrapper.vm.accumulatedTarget).toBe('newtab');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed: anchorTarget', () => {
|
||||
it.each([
|
||||
['sametab', '_self'],
|
||||
['newtab', '_blank'],
|
||||
['parent', '_parent'],
|
||||
['top', '_top'],
|
||||
['modal', undefined],
|
||||
])('target "%s" → %s', (target, expected) => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', target,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.anchorTarget).toBe(expected);
|
||||
});
|
||||
|
||||
it('returns _self in edit mode', () => {
|
||||
const { wrapper } = mountItem({
|
||||
storeState: { editMode: true, config: { appConfig: {} } },
|
||||
});
|
||||
expect(wrapper.vm.anchorTarget).toBe('_self');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed: hyperLinkHref', () => {
|
||||
it('returns "#" in edit mode', () => {
|
||||
const { wrapper } = mountItem({
|
||||
storeState: { editMode: true, config: { appConfig: {} } },
|
||||
});
|
||||
expect(wrapper.vm.hyperLinkHref).toBe('#');
|
||||
});
|
||||
|
||||
it.each(['modal', 'workspace', 'clipboard'])('returns "#" for %s target', (target) => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: 'https://x.com', target,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.hyperLinkHref).toBe('#');
|
||||
});
|
||||
|
||||
it('returns URL for normal targets', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: 'https://x.com', target: 'newtab',
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.hyperLinkHref).toBe('https://x.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed: unicodeOpeningIcon', () => {
|
||||
it.each([
|
||||
['newtab', '"\\f360"'],
|
||||
['sametab', '"\\f24d"'],
|
||||
['parent', '"\\f3bf"'],
|
||||
['top', '"\\f102"'],
|
||||
['modal', '"\\f2d0"'],
|
||||
['workspace', '"\\f0b1"'],
|
||||
['clipboard', '"\\f0ea"'],
|
||||
])('target "%s" → correct icon', (target, expected) => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', target,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.unicodeOpeningIcon).toBe(expected);
|
||||
});
|
||||
|
||||
it('returns default icon for unknown target', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', target: 'unknown',
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.unicodeOpeningIcon).toBe('"\\f054"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filter: shortUrl', () => {
|
||||
const { shortUrl } = Item.filters;
|
||||
|
||||
it('extracts hostname from URL', () => {
|
||||
expect(shortUrl('https://www.example.com/path?q=1')).toBe('www.example.com');
|
||||
});
|
||||
|
||||
it('handles IP addresses', () => {
|
||||
expect(shortUrl('192.168.1.1')).toBe('192.168.1.1');
|
||||
});
|
||||
|
||||
it('returns empty string for falsy input', () => {
|
||||
expect(shortUrl(null)).toBe('');
|
||||
expect(shortUrl(undefined)).toBe('');
|
||||
expect(shortUrl('')).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string for invalid input', () => {
|
||||
expect(shortUrl('not-a-url')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Methods: getTooltipOptions', () => {
|
||||
it('returns empty object when no description or provider', () => {
|
||||
const { wrapper } = mountItem({ item: { id: '1', title: 'X', url: '#' } });
|
||||
expect(wrapper.vm.getTooltipOptions()).toEqual({});
|
||||
});
|
||||
|
||||
it('includes description and provider in content', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', description: 'Desc', provider: 'Prov',
|
||||
},
|
||||
});
|
||||
const { content } = wrapper.vm.getTooltipOptions();
|
||||
expect(content).toContain('Desc');
|
||||
expect(content).toContain('Prov');
|
||||
});
|
||||
|
||||
it('includes hotkey in content', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', description: 'D', hotkey: 3,
|
||||
},
|
||||
});
|
||||
const { content } = wrapper.vm.getTooltipOptions();
|
||||
expect(content).toContain("'3'");
|
||||
});
|
||||
|
||||
it('shows edit text in edit mode', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', description: 'D',
|
||||
},
|
||||
storeState: { editMode: true, config: { appConfig: {} } },
|
||||
});
|
||||
expect(wrapper.vm.getTooltipOptions().content).toBe(
|
||||
'interactive-editor.edit-section.edit-tooltip',
|
||||
);
|
||||
});
|
||||
|
||||
it('placement is "left" when statusResponse exists', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', description: 'D',
|
||||
},
|
||||
});
|
||||
wrapper.vm.statusResponse = { message: 'ok' };
|
||||
expect(wrapper.vm.getTooltipOptions().placement).toBe('left');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Methods: openItemSettings / closeEditMenu', () => {
|
||||
it('openItemSettings sets editMenuOpen, shows modal, commits SET_MODAL_OPEN', () => {
|
||||
const { wrapper, mutations } = mountItem();
|
||||
wrapper.vm.openItemSettings();
|
||||
expect(wrapper.vm.editMenuOpen).toBe(true);
|
||||
expect(wrapper.vm.$modal.show).toHaveBeenCalledWith('EDIT_ITEM');
|
||||
expect(mutations.SET_MODAL_OPEN).toHaveBeenCalledWith(expect.anything(), true);
|
||||
});
|
||||
|
||||
it('closeEditMenu clears editMenuOpen, hides modal, commits SET_MODAL_OPEN(false)', () => {
|
||||
const { wrapper, mutations } = mountItem();
|
||||
wrapper.vm.editMenuOpen = true;
|
||||
wrapper.vm.closeEditMenu();
|
||||
expect(wrapper.vm.editMenuOpen).toBe(false);
|
||||
expect(wrapper.vm.$modal.hide).toHaveBeenCalledWith('EDIT_ITEM');
|
||||
expect(mutations.SET_MODAL_OPEN).toHaveBeenCalledWith(expect.anything(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Methods: openDeleteItem', () => {
|
||||
it('commits REMOVE_ITEM with correct payload', () => {
|
||||
const { wrapper, mutations } = mountItem({ parentSection: { name: 'MySection' } });
|
||||
wrapper.vm.openDeleteItem();
|
||||
expect(mutations.REMOVE_ITEM).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
{ itemId: 'test-1', sectionName: 'MySection' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Methods: itemClicked', () => {
|
||||
const event = (extra = {}) => ({
|
||||
preventDefault: vi.fn(), ctrlKey: false, altKey: false, ...extra,
|
||||
});
|
||||
|
||||
it('in edit mode: preventDefault + openItemSettings', () => {
|
||||
const { wrapper } = mountItem({ storeState: { editMode: true, config: { appConfig: {} } } });
|
||||
const e = event();
|
||||
const spy = vi.spyOn(wrapper.vm, 'openItemSettings');
|
||||
wrapper.vm.itemClicked(e);
|
||||
expect(e.preventDefault).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ctrl key: opens in new tab', () => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.itemClicked(event({ ctrlKey: true }));
|
||||
expect(openSpy).toHaveBeenCalledWith('https://example.com', '_blank');
|
||||
});
|
||||
|
||||
it('alt key: emits triggerModal', () => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.itemClicked(event({ altKey: true }));
|
||||
expect(wrapper.emitted().triggerModal).toBeTruthy();
|
||||
});
|
||||
|
||||
it('target modal: emits triggerModal', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: 'https://x.com', target: 'modal',
|
||||
},
|
||||
});
|
||||
wrapper.vm.itemClicked(event());
|
||||
expect(wrapper.emitted().triggerModal).toBeTruthy();
|
||||
});
|
||||
|
||||
it('target workspace: calls router.push', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: 'https://x.com', target: 'workspace',
|
||||
},
|
||||
});
|
||||
wrapper.vm.itemClicked(event());
|
||||
expect(router.push).toHaveBeenCalledWith({ name: 'workspace', query: { url: 'https://x.com' } });
|
||||
});
|
||||
|
||||
it('target clipboard: calls copyToClipboard', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: 'https://x.com', target: 'clipboard',
|
||||
},
|
||||
});
|
||||
const spy = vi.spyOn(wrapper.vm, 'copyToClipboard');
|
||||
wrapper.vm.itemClicked(event());
|
||||
expect(spy).toHaveBeenCalledWith('https://x.com');
|
||||
});
|
||||
|
||||
it('always emits itemClicked', () => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.itemClicked(event());
|
||||
expect(wrapper.emitted().itemClicked).toBeTruthy();
|
||||
});
|
||||
|
||||
it('skips smart-sort when disableSmartSort is set', () => {
|
||||
const { wrapper } = mountItem({ appConfig: { disableSmartSort: true } });
|
||||
const spy = vi.spyOn(wrapper.vm, 'incrementMostUsedCount');
|
||||
wrapper.vm.itemClicked(event());
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Methods: launchItem', () => {
|
||||
it.each([
|
||||
['newtab', '_blank'],
|
||||
['sametab', '_self'],
|
||||
])('%s calls window.open with %s', (method, target) => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.launchItem(method, 'https://test.com');
|
||||
expect(openSpy).toHaveBeenCalledWith('https://test.com', target);
|
||||
});
|
||||
|
||||
it('modal emits triggerModal', () => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.launchItem('modal', 'https://test.com');
|
||||
expect(wrapper.emitted().triggerModal[0]).toEqual(['https://test.com']);
|
||||
});
|
||||
|
||||
it('workspace calls router.push', () => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.launchItem('workspace', 'https://test.com');
|
||||
expect(router.push).toHaveBeenCalledWith({ name: 'workspace', query: { url: 'https://test.com' } });
|
||||
});
|
||||
|
||||
it('clipboard calls copyToClipboard', () => {
|
||||
const { wrapper } = mountItem();
|
||||
const spy = vi.spyOn(wrapper.vm, 'copyToClipboard');
|
||||
wrapper.vm.launchItem('clipboard', 'https://test.com');
|
||||
expect(spy).toHaveBeenCalledWith('https://test.com');
|
||||
});
|
||||
|
||||
it('closes context menu', () => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.contextMenuOpen = true;
|
||||
wrapper.vm.launchItem('newtab');
|
||||
expect(wrapper.vm.contextMenuOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('falls back to item.url when no link arg', () => {
|
||||
const { wrapper } = mountItem({
|
||||
item: { id: '1', title: 'X', url: 'https://fallback.com' },
|
||||
});
|
||||
wrapper.vm.launchItem('newtab');
|
||||
expect(openSpy).toHaveBeenCalledWith('https://fallback.com', '_blank');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Methods: openContextMenu / closeContextMenu', () => {
|
||||
it('toggles contextMenuOpen and sets position', () => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.openContextMenu({ clientX: 100, clientY: 200 });
|
||||
expect(wrapper.vm.contextMenuOpen).toBe(true);
|
||||
expect(wrapper.vm.contextPos.posX).toBe(100 + window.pageXOffset);
|
||||
expect(wrapper.vm.contextPos.posY).toBe(200 + window.pageYOffset);
|
||||
});
|
||||
|
||||
it('closeContextMenu sets false', () => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.contextMenuOpen = true;
|
||||
wrapper.vm.closeContextMenu();
|
||||
expect(wrapper.vm.contextMenuOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Methods: copyToClipboard', () => {
|
||||
it('calls navigator.clipboard.writeText and shows toast', () => {
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.copyToClipboard('hello');
|
||||
expect(clipboardSpy).toHaveBeenCalledWith('hello');
|
||||
expect(wrapper.vm.$toasted.show).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows error when clipboard unavailable', async () => {
|
||||
const ErrorHandler = (await import('@/utils/ErrorHandler')).default;
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined, writable: true, configurable: true,
|
||||
});
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.copyToClipboard('hello');
|
||||
expect(ErrorHandler).toHaveBeenCalled();
|
||||
expect(wrapper.vm.$toasted.show).toHaveBeenCalledWith(
|
||||
'Unable to copy, see log',
|
||||
expect.objectContaining({ className: 'toast-error' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Methods: incrementMostUsedCount / incrementLastUsedCount', () => {
|
||||
it('increments existing count', () => {
|
||||
localStorage.getItem.mockReturnValue(JSON.stringify({ 'item-1': 5 }));
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.incrementMostUsedCount('item-1');
|
||||
const saved = JSON.parse(localStorage.setItem.mock.calls[0][1]);
|
||||
expect(saved['item-1']).toBe(6);
|
||||
});
|
||||
|
||||
it('initializes new items to 1', () => {
|
||||
localStorage.getItem.mockReturnValue('{}');
|
||||
const { wrapper } = mountItem();
|
||||
wrapper.vm.incrementMostUsedCount('new-item');
|
||||
const saved = JSON.parse(localStorage.setItem.mock.calls[0][1]);
|
||||
expect(saved['new-item']).toBe(1);
|
||||
});
|
||||
|
||||
it('writes last-used timestamp', () => {
|
||||
localStorage.getItem.mockReturnValue('{}');
|
||||
const { wrapper } = mountItem();
|
||||
const before = Date.now();
|
||||
wrapper.vm.incrementLastUsedCount('item-1');
|
||||
const saved = JSON.parse(localStorage.setItem.mock.calls[0][1]);
|
||||
expect(saved['item-1']).toBeGreaterThanOrEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lifecycle: mounted', () => {
|
||||
it('calls checkWebsiteStatus when enableStatusCheck is true', () => {
|
||||
const spy = vi.spyOn(Item.mixins[0].methods, 'checkWebsiteStatus');
|
||||
mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: 'https://x.com', statusCheck: true,
|
||||
},
|
||||
});
|
||||
expect(spy).toHaveBeenCalled();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('sets up interval when statusCheckInterval > 0', () => {
|
||||
vi.useFakeTimers();
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: 'https://x.com', statusCheck: true, statusCheckInterval: 5,
|
||||
},
|
||||
});
|
||||
expect(wrapper.vm.intervalId).toBeDefined();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('does nothing when statusCheck disabled', () => {
|
||||
const spy = vi.spyOn(Item.mixins[0].methods, 'checkWebsiteStatus');
|
||||
mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: 'https://x.com', statusCheck: false,
|
||||
},
|
||||
});
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lifecycle: beforeDestroy', () => {
|
||||
it('clears interval if intervalId exists', () => {
|
||||
vi.useFakeTimers();
|
||||
const clearSpy = vi.spyOn(global, 'clearInterval');
|
||||
const { wrapper } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: 'https://x.com', statusCheck: true, statusCheckInterval: 5,
|
||||
},
|
||||
});
|
||||
const { intervalId } = wrapper.vm;
|
||||
wrapper.destroy();
|
||||
expect(clearSpy).toHaveBeenCalledWith(intervalId);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Template rendering', () => {
|
||||
it('renders item title and description', () => {
|
||||
const { wrapper } = mountItem();
|
||||
expect(wrapper.find('.text').text()).toBe('Test Item');
|
||||
expect(wrapper.find('.description').text()).toBe('A test description');
|
||||
});
|
||||
|
||||
it('has correct wrapper classes', () => {
|
||||
const { wrapper } = mountItem({ props: { itemSize: 'large', sectionWidth: 800 } });
|
||||
const div = wrapper.find('.item-wrapper');
|
||||
expect(div.classes()).toContain('wrap-size-large');
|
||||
expect(div.classes()).toContain('span-4');
|
||||
});
|
||||
|
||||
it('shows StatusIndicator only when enableStatusCheck', () => {
|
||||
const { wrapper: off } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', statusCheck: false,
|
||||
},
|
||||
});
|
||||
expect(off.find('statusindicator-stub').exists()).toBe(false);
|
||||
|
||||
const { wrapper: on } = mountItem({
|
||||
item: {
|
||||
id: '1', title: 'X', url: '#', statusCheck: true,
|
||||
},
|
||||
});
|
||||
expect(on.find('statusindicator-stub').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows EditModeIcon only in edit mode', () => {
|
||||
const { wrapper: normal } = mountItem();
|
||||
expect(normal.find('editmodeicon-stub').exists()).toBe(false);
|
||||
|
||||
const { wrapper: editing } = mountItem({
|
||||
storeState: { editMode: true, config: { appConfig: {} } },
|
||||
});
|
||||
expect(editing.find('editmodeicon-stub').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets correct id on anchor', () => {
|
||||
const { wrapper } = mountItem();
|
||||
expect(wrapper.find('a.item').attributes('id')).toBe('link-test-1');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
110
tests/setup.js
110
tests/setup.js
|
|
@ -1,54 +1,56 @@
|
|||
/**
|
||||
* Global test setup file
|
||||
* This file is run before all tests to configure the testing environment
|
||||
*/
|
||||
|
||||
import { config } from '@vue/test-utils';
|
||||
|
||||
// Suppress Vue warnings in tests
|
||||
config.silent = true;
|
||||
|
||||
// Mock console methods to avoid noise in test output
|
||||
global.console = {
|
||||
...console,
|
||||
// Uncomment to suppress console.log in tests
|
||||
// log: vi.fn(),
|
||||
// Uncomment to suppress console.debug in tests
|
||||
// debug: vi.fn(),
|
||||
// Keep warnings and errors visible
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
};
|
||||
|
||||
// Mock localStorage for tests
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
global.localStorage = localStorageMock;
|
||||
|
||||
// Mock sessionStorage for tests
|
||||
const sessionStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
global.sessionStorage = sessionStorageMock;
|
||||
|
||||
// Mock window.matchMedia (for responsive design tests)
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
/**
|
||||
* Global test setup file
|
||||
* This file is run before all tests to configure the testing environment
|
||||
*/
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
// Suppress Vue warnings in tests
|
||||
Vue.config.silent = true;
|
||||
|
||||
// Suppress noisy console methods in test output
|
||||
// Vue dev mode prints info messages (devtools, production tips) that clutter results
|
||||
global.console = {
|
||||
...console,
|
||||
info: vi.fn(),
|
||||
// Uncomment to suppress console.log in tests
|
||||
// log: vi.fn(),
|
||||
// Uncomment to suppress console.debug in tests
|
||||
// debug: vi.fn(),
|
||||
// Keep warnings and errors visible
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
};
|
||||
|
||||
// Mock localStorage for tests
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
global.localStorage = localStorageMock;
|
||||
|
||||
// Mock sessionStorage for tests
|
||||
const sessionStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
global.sessionStorage = sessionStorageMock;
|
||||
|
||||
// Mock window.matchMedia (for responsive design tests)
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,128 +1,128 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
makePageName,
|
||||
makePageSlug,
|
||||
formatConfigPath,
|
||||
componentVisibility,
|
||||
getCustomKeyShortcuts,
|
||||
} from '@/utils/ConfigHelpers';
|
||||
|
||||
describe('ConfigHelpers - makePageName', () => {
|
||||
it('converts page name to lowercase', () => {
|
||||
expect(makePageName('My Page')).toBe('my-page');
|
||||
});
|
||||
|
||||
it('replaces spaces with hyphens', () => {
|
||||
expect(makePageName('Multiple Word Page')).toBe('multiple-word-page');
|
||||
});
|
||||
|
||||
it('removes .yml extension', () => {
|
||||
expect(makePageName('config.yml')).toBe('config');
|
||||
});
|
||||
|
||||
it('removes special characters', () => {
|
||||
expect(makePageName('Page!@#$Name')).toBe('pagename');
|
||||
});
|
||||
|
||||
it('handles undefined input', () => {
|
||||
expect(makePageName(undefined)).toBe('unnamed-page');
|
||||
});
|
||||
|
||||
it('handles null input', () => {
|
||||
expect(makePageName(null)).toBe('unnamed-page');
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(makePageName('')).toBe('unnamed-page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigHelpers - makePageSlug', () => {
|
||||
it('creates correct slug format', () => {
|
||||
expect(makePageSlug('My Page', 'home')).toBe('/home/my-page');
|
||||
});
|
||||
|
||||
it('handles page names with special chars', () => {
|
||||
expect(makePageSlug('Config! Page', 'admin')).toBe('/admin/config-page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigHelpers - formatConfigPath', () => {
|
||||
it('leaves http URLs unchanged', () => {
|
||||
const url = 'https://example.com/config.yml';
|
||||
expect(formatConfigPath(url)).toBe(url);
|
||||
});
|
||||
|
||||
it('adds leading slash to relative paths', () => {
|
||||
expect(formatConfigPath('config.yml')).toBe('/config.yml');
|
||||
});
|
||||
|
||||
it('keeps absolute paths unchanged', () => {
|
||||
expect(formatConfigPath('/config.yml')).toBe('/config.yml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigHelpers - componentVisibility', () => {
|
||||
it('returns all visible by default when no config', () => {
|
||||
const result = componentVisibility({});
|
||||
expect(result.pageTitle).toBe(true);
|
||||
expect(result.navigation).toBe(true);
|
||||
expect(result.searchBar).toBe(true);
|
||||
expect(result.settings).toBe(true);
|
||||
expect(result.footer).toBe(true);
|
||||
});
|
||||
|
||||
it('hides components based on config', () => {
|
||||
const appConfig = {
|
||||
hideComponents: {
|
||||
hideHeading: true,
|
||||
hideNav: true,
|
||||
},
|
||||
};
|
||||
const result = componentVisibility(appConfig);
|
||||
expect(result.pageTitle).toBe(false);
|
||||
expect(result.navigation).toBe(false);
|
||||
expect(result.searchBar).toBe(true);
|
||||
});
|
||||
|
||||
it('handles partial config correctly', () => {
|
||||
const appConfig = {
|
||||
hideComponents: {
|
||||
hideFooter: true,
|
||||
},
|
||||
};
|
||||
const result = componentVisibility(appConfig);
|
||||
expect(result.footer).toBe(false);
|
||||
expect(result.pageTitle).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigHelpers - getCustomKeyShortcuts', () => {
|
||||
it('extracts hotkeys from sections', () => {
|
||||
const sections = [
|
||||
{
|
||||
items: [
|
||||
{ hotkey: 1, url: 'https://example.com' },
|
||||
{ url: 'https://example.org' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = getCustomKeyShortcuts(sections);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({ hotkey: 1, url: 'https://example.com' });
|
||||
});
|
||||
|
||||
it('returns empty array when no hotkeys', () => {
|
||||
const sections = [{ items: [{ url: 'https://example.com' }] }];
|
||||
expect(getCustomKeyShortcuts(sections)).toEqual([]);
|
||||
});
|
||||
|
||||
it('flattens hotkeys from multiple sections', () => {
|
||||
const sections = [
|
||||
{ items: [{ hotkey: 1, url: 'https://a.com' }] },
|
||||
{ items: [{ hotkey: 2, url: 'https://b.com' }] },
|
||||
];
|
||||
const result = getCustomKeyShortcuts(sections);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
makePageName,
|
||||
makePageSlug,
|
||||
formatConfigPath,
|
||||
componentVisibility,
|
||||
getCustomKeyShortcuts,
|
||||
} from '@/utils/ConfigHelpers';
|
||||
|
||||
describe('ConfigHelpers - makePageName', () => {
|
||||
it('converts page name to lowercase', () => {
|
||||
expect(makePageName('My Page')).toBe('my-page');
|
||||
});
|
||||
|
||||
it('replaces spaces with hyphens', () => {
|
||||
expect(makePageName('Multiple Word Page')).toBe('multiple-word-page');
|
||||
});
|
||||
|
||||
it('removes .yml extension', () => {
|
||||
expect(makePageName('config.yml')).toBe('config');
|
||||
});
|
||||
|
||||
it('removes special characters', () => {
|
||||
expect(makePageName('Page!@#$Name')).toBe('pagename');
|
||||
});
|
||||
|
||||
it('handles undefined input', () => {
|
||||
expect(makePageName(undefined)).toBe('unnamed-page');
|
||||
});
|
||||
|
||||
it('handles null input', () => {
|
||||
expect(makePageName(null)).toBe('unnamed-page');
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(makePageName('')).toBe('unnamed-page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigHelpers - makePageSlug', () => {
|
||||
it('creates correct slug format', () => {
|
||||
expect(makePageSlug('My Page', 'home')).toBe('/home/my-page');
|
||||
});
|
||||
|
||||
it('handles page names with special chars', () => {
|
||||
expect(makePageSlug('Config! Page', 'admin')).toBe('/admin/config-page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigHelpers - formatConfigPath', () => {
|
||||
it('leaves http URLs unchanged', () => {
|
||||
const url = 'https://example.com/config.yml';
|
||||
expect(formatConfigPath(url)).toBe(url);
|
||||
});
|
||||
|
||||
it('adds leading slash to relative paths', () => {
|
||||
expect(formatConfigPath('config.yml')).toBe('/config.yml');
|
||||
});
|
||||
|
||||
it('keeps absolute paths unchanged', () => {
|
||||
expect(formatConfigPath('/config.yml')).toBe('/config.yml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigHelpers - componentVisibility', () => {
|
||||
it('returns all visible by default when no config', () => {
|
||||
const result = componentVisibility({});
|
||||
expect(result.pageTitle).toBe(true);
|
||||
expect(result.navigation).toBe(true);
|
||||
expect(result.searchBar).toBe(true);
|
||||
expect(result.settings).toBe(true);
|
||||
expect(result.footer).toBe(true);
|
||||
});
|
||||
|
||||
it('hides components based on config', () => {
|
||||
const appConfig = {
|
||||
hideComponents: {
|
||||
hideHeading: true,
|
||||
hideNav: true,
|
||||
},
|
||||
};
|
||||
const result = componentVisibility(appConfig);
|
||||
expect(result.pageTitle).toBe(false);
|
||||
expect(result.navigation).toBe(false);
|
||||
expect(result.searchBar).toBe(true);
|
||||
});
|
||||
|
||||
it('handles partial config correctly', () => {
|
||||
const appConfig = {
|
||||
hideComponents: {
|
||||
hideFooter: true,
|
||||
},
|
||||
};
|
||||
const result = componentVisibility(appConfig);
|
||||
expect(result.footer).toBe(false);
|
||||
expect(result.pageTitle).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigHelpers - getCustomKeyShortcuts', () => {
|
||||
it('extracts hotkeys from sections', () => {
|
||||
const sections = [
|
||||
{
|
||||
items: [
|
||||
{ hotkey: 1, url: 'https://example.com' },
|
||||
{ url: 'https://example.org' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = getCustomKeyShortcuts(sections);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({ hotkey: 1, url: 'https://example.com' });
|
||||
});
|
||||
|
||||
it('returns empty array when no hotkeys', () => {
|
||||
const sections = [{ items: [{ url: 'https://example.com' }] }];
|
||||
expect(getCustomKeyShortcuts(sections)).toEqual([]);
|
||||
});
|
||||
|
||||
it('flattens hotkeys from multiple sections', () => {
|
||||
const sections = [
|
||||
{ items: [{ hotkey: 1, url: 'https://a.com' }] },
|
||||
{ items: [{ hotkey: 2, url: 'https://b.com' }] },
|
||||
];
|
||||
const result = getCustomKeyShortcuts(sections);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,75 +1,77 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Config Validator', () => {
|
||||
const configValidator = path.resolve(__dirname, '../../services/config-validator.js');
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.VUE_APP_CONFIG_VALID;
|
||||
});
|
||||
|
||||
it('validates a correct config file', () => {
|
||||
const yaml = require('js-yaml');
|
||||
const Ajv = require('ajv');
|
||||
const schema = require('../../src/utils/ConfigSchema.json');
|
||||
|
||||
const validConfig = {
|
||||
pageInfo: { title: 'Test' },
|
||||
appConfig: {},
|
||||
sections: [{ name: 'Test', items: [{ title: 'Item', url: 'https://example.com' }] }],
|
||||
};
|
||||
|
||||
const ajv = new Ajv({ strict: false, allowUnionTypes: true, allErrors: true });
|
||||
const valid = ajv.validate(schema, validConfig);
|
||||
expect(valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects config with invalid structure', () => {
|
||||
const Ajv = require('ajv');
|
||||
const schema = require('../../src/utils/ConfigSchema.json');
|
||||
|
||||
const invalidConfig = {
|
||||
pageInfo: { title: 'Test' },
|
||||
sections: 'not an array',
|
||||
};
|
||||
|
||||
const ajv = new Ajv({ strict: false, allowUnionTypes: true, allErrors: true });
|
||||
const valid = ajv.validate(schema, invalidConfig);
|
||||
expect(valid).toBe(false);
|
||||
expect(ajv.errors).toBeTruthy();
|
||||
});
|
||||
|
||||
it('requires sections to be an array', () => {
|
||||
const Ajv = require('ajv');
|
||||
const schema = require('../../src/utils/ConfigSchema.json');
|
||||
|
||||
const config = {
|
||||
pageInfo: { title: 'Test' },
|
||||
sections: {},
|
||||
};
|
||||
|
||||
const ajv = new Ajv({ strict: false, allowUnionTypes: true, allErrors: true });
|
||||
const valid = ajv.validate(schema, config);
|
||||
expect(valid).toBe(false);
|
||||
});
|
||||
|
||||
it('allows items with just title', () => {
|
||||
const Ajv = require('ajv');
|
||||
const schema = require('../../src/utils/ConfigSchema.json');
|
||||
|
||||
const config = {
|
||||
pageInfo: { title: 'Test' },
|
||||
sections: [
|
||||
{
|
||||
name: 'Test Section',
|
||||
items: [{ title: 'Item', url: 'https://example.com' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ajv = new Ajv({ strict: false, allowUnionTypes: true, allErrors: true });
|
||||
const valid = ajv.validate(schema, config);
|
||||
expect(valid).toBe(true);
|
||||
});
|
||||
});
|
||||
import {
|
||||
describe, it, expect, beforeEach, vi,
|
||||
} from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Config Validator', () => {
|
||||
const configValidator = path.resolve(__dirname, '../../services/config-validator.js');
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.VUE_APP_CONFIG_VALID;
|
||||
});
|
||||
|
||||
it('validates a correct config file', () => {
|
||||
const yaml = require('js-yaml');
|
||||
const Ajv = require('ajv');
|
||||
const schema = require('../../src/utils/ConfigSchema.json');
|
||||
|
||||
const validConfig = {
|
||||
pageInfo: { title: 'Test' },
|
||||
appConfig: {},
|
||||
sections: [{ name: 'Test', items: [{ title: 'Item', url: 'https://example.com' }] }],
|
||||
};
|
||||
|
||||
const ajv = new Ajv({ strict: false, allowUnionTypes: true, allErrors: true });
|
||||
const valid = ajv.validate(schema, validConfig);
|
||||
expect(valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects config with invalid structure', () => {
|
||||
const Ajv = require('ajv');
|
||||
const schema = require('../../src/utils/ConfigSchema.json');
|
||||
|
||||
const invalidConfig = {
|
||||
pageInfo: { title: 'Test' },
|
||||
sections: 'not an array',
|
||||
};
|
||||
|
||||
const ajv = new Ajv({ strict: false, allowUnionTypes: true, allErrors: true });
|
||||
const valid = ajv.validate(schema, invalidConfig);
|
||||
expect(valid).toBe(false);
|
||||
expect(ajv.errors).toBeTruthy();
|
||||
});
|
||||
|
||||
it('requires sections to be an array', () => {
|
||||
const Ajv = require('ajv');
|
||||
const schema = require('../../src/utils/ConfigSchema.json');
|
||||
|
||||
const config = {
|
||||
pageInfo: { title: 'Test' },
|
||||
sections: {},
|
||||
};
|
||||
|
||||
const ajv = new Ajv({ strict: false, allowUnionTypes: true, allErrors: true });
|
||||
const valid = ajv.validate(schema, config);
|
||||
expect(valid).toBe(false);
|
||||
});
|
||||
|
||||
it('allows items with just title', () => {
|
||||
const Ajv = require('ajv');
|
||||
const schema = require('../../src/utils/ConfigSchema.json');
|
||||
|
||||
const config = {
|
||||
pageInfo: { title: 'Test' },
|
||||
sections: [
|
||||
{
|
||||
name: 'Test Section',
|
||||
items: [{ title: 'Item', url: 'https://example.com' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ajv = new Ajv({ strict: false, allowUnionTypes: true, allErrors: true });
|
||||
const valid = ajv.validate(schema, config);
|
||||
expect(valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ErrorHandler', () => {
|
||||
it('exports InfoKeys constants', async () => {
|
||||
const { InfoKeys } = await import('@/utils/ErrorHandler');
|
||||
expect(InfoKeys.AUTH).toBe('Authentication');
|
||||
expect(InfoKeys.CLOUD_BACKUP).toBe('Cloud Backup & Restore');
|
||||
expect(InfoKeys.EDITOR).toBe('Interactive Editor');
|
||||
expect(InfoKeys.RAW_EDITOR).toBe('Raw Config Editor');
|
||||
expect(InfoKeys.VISUAL).toBe('Layout & Styles');
|
||||
});
|
||||
|
||||
it('exports handler functions', async () => {
|
||||
const handlers = await import('@/utils/ErrorHandler');
|
||||
expect(typeof handlers.ErrorHandler).toBe('function');
|
||||
expect(typeof handlers.InfoHandler).toBe('function');
|
||||
expect(typeof handlers.WarningInfoHandler).toBe('function');
|
||||
});
|
||||
|
||||
it('ErrorHandler can be called without throwing', async () => {
|
||||
const { ErrorHandler } = await import('@/utils/ErrorHandler');
|
||||
expect(() => ErrorHandler('Test error')).not.toThrow();
|
||||
});
|
||||
});
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ErrorHandler', () => {
|
||||
it('exports InfoKeys constants', async () => {
|
||||
const { InfoKeys } = await import('@/utils/ErrorHandler');
|
||||
expect(InfoKeys.AUTH).toBe('Authentication');
|
||||
expect(InfoKeys.CLOUD_BACKUP).toBe('Cloud Backup & Restore');
|
||||
expect(InfoKeys.EDITOR).toBe('Interactive Editor');
|
||||
expect(InfoKeys.RAW_EDITOR).toBe('Raw Config Editor');
|
||||
expect(InfoKeys.VISUAL).toBe('Layout & Styles');
|
||||
});
|
||||
|
||||
it('exports handler functions', async () => {
|
||||
const handlers = await import('@/utils/ErrorHandler');
|
||||
expect(typeof handlers.ErrorHandler).toBe('function');
|
||||
expect(typeof handlers.InfoHandler).toBe('function');
|
||||
expect(typeof handlers.WarningInfoHandler).toBe('function');
|
||||
});
|
||||
|
||||
it('ErrorHandler can be called without throwing', async () => {
|
||||
const { ErrorHandler } = await import('@/utils/ErrorHandler');
|
||||
expect(() => ErrorHandler('Test error')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Healthcheck Service', () => {
|
||||
it('healthcheck script exists', () => {
|
||||
const healthcheckPath = path.resolve(__dirname, '../../services/healthcheck.js');
|
||||
expect(fs.existsSync(healthcheckPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('healthcheck file has correct structure', () => {
|
||||
const healthcheckPath = path.resolve(__dirname, '../../services/healthcheck.js');
|
||||
const content = fs.readFileSync(healthcheckPath, 'utf8');
|
||||
expect(content).toContain('healthCheck');
|
||||
expect(content).toContain('http.request');
|
||||
});
|
||||
});
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Healthcheck Service', () => {
|
||||
it('healthcheck script exists', () => {
|
||||
const healthcheckPath = path.resolve(__dirname, '../../services/healthcheck.js');
|
||||
expect(fs.existsSync(healthcheckPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('healthcheck file has correct structure', () => {
|
||||
const healthcheckPath = path.resolve(__dirname, '../../services/healthcheck.js');
|
||||
const content = fs.readFileSync(healthcheckPath, 'utf8');
|
||||
expect(content).toContain('healthCheck');
|
||||
expect(content).toContain('http.request');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,115 +1,115 @@
|
|||
/**
|
||||
* Smoke Tests
|
||||
* Basic tests to verify that the testing infrastructure is working correctly
|
||||
* and that core functionality is operational
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
describe('Smoke Tests - Testing Infrastructure', () => {
|
||||
it('should run a basic test', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('should perform basic math', () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
|
||||
it('should handle strings correctly', () => {
|
||||
expect('dashy').toMatch(/dash/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Smoke Tests - Project Files', () => {
|
||||
it('should have a package.json file', () => {
|
||||
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
||||
expect(fs.existsSync(packageJsonPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have a valid package.json', () => {
|
||||
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
expect(packageJson.name).toBe('dashy');
|
||||
expect(packageJson.version).toBeDefined();
|
||||
expect(packageJson.license).toBe('MIT');
|
||||
});
|
||||
|
||||
it('should have a server.js file', () => {
|
||||
const serverPath = path.resolve(__dirname, '../../server.js');
|
||||
expect(fs.existsSync(serverPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have src directory', () => {
|
||||
const srcPath = path.resolve(__dirname, '../../src');
|
||||
expect(fs.existsSync(srcPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Smoke Tests - Config Loading', () => {
|
||||
it('should parse a valid YAML config file', () => {
|
||||
const configPath = path.resolve(__dirname, '../fixtures/valid-config.yml');
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config.pageInfo).toBeDefined();
|
||||
expect(config.pageInfo.title).toBe('Test Dashboard');
|
||||
});
|
||||
|
||||
it('should have required config structure', () => {
|
||||
const configPath = path.resolve(__dirname, '../fixtures/valid-config.yml');
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
|
||||
// Check required top-level properties
|
||||
expect(config).toHaveProperty('pageInfo');
|
||||
expect(config).toHaveProperty('appConfig');
|
||||
expect(config).toHaveProperty('sections');
|
||||
|
||||
// Check sections structure
|
||||
expect(Array.isArray(config.sections)).toBe(true);
|
||||
expect(config.sections.length).toBeGreaterThan(0);
|
||||
|
||||
// Check first section has items
|
||||
const firstSection = config.sections[0];
|
||||
expect(firstSection).toHaveProperty('name');
|
||||
expect(firstSection).toHaveProperty('items');
|
||||
expect(Array.isArray(firstSection.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate item structure in config', () => {
|
||||
const configPath = path.resolve(__dirname, '../fixtures/valid-config.yml');
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
|
||||
const firstItem = config.sections[0].items[0];
|
||||
|
||||
// Each item should have required properties
|
||||
expect(firstItem).toHaveProperty('title');
|
||||
expect(firstItem).toHaveProperty('url');
|
||||
|
||||
// URL should be valid
|
||||
expect(firstItem.url).toMatch(/^https?:\/\//);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Smoke Tests - Core Dependencies', () => {
|
||||
it('should load yaml parser', () => {
|
||||
expect(yaml).toBeDefined();
|
||||
expect(typeof yaml.load).toBe('function');
|
||||
});
|
||||
|
||||
it('should load fs module', () => {
|
||||
expect(fs).toBeDefined();
|
||||
expect(typeof fs.readFileSync).toBe('function');
|
||||
});
|
||||
|
||||
it('should have config schema file', () => {
|
||||
const schemaPath = path.resolve(__dirname, '../../src/utils/ConfigSchema.json');
|
||||
expect(fs.existsSync(schemaPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Smoke Tests
|
||||
* Basic tests to verify that the testing infrastructure is working correctly
|
||||
* and that core functionality is operational
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
describe('Smoke Tests - Testing Infrastructure', () => {
|
||||
it('should run a basic test', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('should perform basic math', () => {
|
||||
expect(2 + 2).toBe(4);
|
||||
});
|
||||
|
||||
it('should handle strings correctly', () => {
|
||||
expect('dashy').toMatch(/dash/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Smoke Tests - Project Files', () => {
|
||||
it('should have a package.json file', () => {
|
||||
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
||||
expect(fs.existsSync(packageJsonPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have a valid package.json', () => {
|
||||
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
expect(packageJson.name).toBe('dashy');
|
||||
expect(packageJson.version).toBeDefined();
|
||||
expect(packageJson.license).toBe('MIT');
|
||||
});
|
||||
|
||||
it('should have a server.js file', () => {
|
||||
const serverPath = path.resolve(__dirname, '../../server.js');
|
||||
expect(fs.existsSync(serverPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have src directory', () => {
|
||||
const srcPath = path.resolve(__dirname, '../../src');
|
||||
expect(fs.existsSync(srcPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Smoke Tests - Config Loading', () => {
|
||||
it('should parse a valid YAML config file', () => {
|
||||
const configPath = path.resolve(__dirname, '../fixtures/valid-config.yml');
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config.pageInfo).toBeDefined();
|
||||
expect(config.pageInfo.title).toBe('Test Dashboard');
|
||||
});
|
||||
|
||||
it('should have required config structure', () => {
|
||||
const configPath = path.resolve(__dirname, '../fixtures/valid-config.yml');
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
|
||||
// Check required top-level properties
|
||||
expect(config).toHaveProperty('pageInfo');
|
||||
expect(config).toHaveProperty('appConfig');
|
||||
expect(config).toHaveProperty('sections');
|
||||
|
||||
// Check sections structure
|
||||
expect(Array.isArray(config.sections)).toBe(true);
|
||||
expect(config.sections.length).toBeGreaterThan(0);
|
||||
|
||||
// Check first section has items
|
||||
const firstSection = config.sections[0];
|
||||
expect(firstSection).toHaveProperty('name');
|
||||
expect(firstSection).toHaveProperty('items');
|
||||
expect(Array.isArray(firstSection.items)).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate item structure in config', () => {
|
||||
const configPath = path.resolve(__dirname, '../fixtures/valid-config.yml');
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
const config = yaml.load(configContent);
|
||||
|
||||
const firstItem = config.sections[0].items[0];
|
||||
|
||||
// Each item should have required properties
|
||||
expect(firstItem).toHaveProperty('title');
|
||||
expect(firstItem).toHaveProperty('url');
|
||||
|
||||
// URL should be valid
|
||||
expect(firstItem.url).toMatch(/^https?:\/\//);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Smoke Tests - Core Dependencies', () => {
|
||||
it('should load yaml parser', () => {
|
||||
expect(yaml).toBeDefined();
|
||||
expect(typeof yaml.load).toBe('function');
|
||||
});
|
||||
|
||||
it('should load fs module', () => {
|
||||
expect(fs).toBeDefined();
|
||||
expect(typeof fs.readFileSync).toBe('function');
|
||||
});
|
||||
|
||||
it('should have config schema file', () => {
|
||||
const schemaPath = path.resolve(__dirname, '../../src/utils/ConfigSchema.json');
|
||||
expect(fs.existsSync(schemaPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,40 +1,43 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// Use happy-dom for faster DOM simulation
|
||||
environment: 'happy-dom',
|
||||
|
||||
// Make test functions available globally (describe, it, expect, etc.)
|
||||
globals: true,
|
||||
|
||||
// Setup file for global test configuration
|
||||
setupFiles: ['./tests/setup.js'],
|
||||
|
||||
// Include patterns
|
||||
include: ['tests/**/*.{test,spec}.{js,ts}', 'src/**/*.{test,spec}.{js,ts}'],
|
||||
|
||||
// Coverage configuration
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'tests/',
|
||||
'*.config.js',
|
||||
'dist/',
|
||||
'.github/',
|
||||
'docs/',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
resolve: {
|
||||
// Match the alias configuration from vue.config.js
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'vue': 'vue/dist/vue.esm.js', // Use the full build for tests
|
||||
},
|
||||
},
|
||||
});
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { createVuePlugin } from 'vite-plugin-vue2';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [createVuePlugin()],
|
||||
test: {
|
||||
// Use happy-dom for faster DOM simulation
|
||||
environment: 'happy-dom',
|
||||
|
||||
// Make test functions available globally (describe, it, expect, etc.)
|
||||
globals: true,
|
||||
|
||||
// Setup file for global test configuration
|
||||
setupFiles: ['./tests/setup.js'],
|
||||
|
||||
// Include patterns
|
||||
include: ['tests/**/*.{test,spec}.{js,ts}', 'src/**/*.{test,spec}.{js,ts}'],
|
||||
|
||||
// Coverage configuration
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'tests/',
|
||||
'*.config.js',
|
||||
'dist/',
|
||||
'.github/',
|
||||
'docs/',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
// Match the alias configuration from vue.config.js
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
vue: 'vue/dist/vue.esm.js', // Use the full build for tests
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue