Skip to content

Commit c9f6f33

Browse files
authored
Merge branch 'main' into fix/libvirt-install
2 parents 7813cd8 + 36a7a28 commit c9f6f33

36 files changed

+1418
-1203
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
---
2+
description:
3+
globs: **/*.test.ts,**/__test__/components/**/*.ts,**/__test__/store/**/*.ts,**/__test__/mocks/**/*.ts
4+
alwaysApply: false
5+
---
6+
7+
## Vue Component Testing Best Practices
8+
9+
- Use pnpm when running termical commands and stay within the web directory.
10+
- The directory for tests is located under `web/test`
11+
12+
### Setup
13+
- Use `mount` from Vue Test Utils for component testing
14+
- Stub complex child components that aren't the focus of the test
15+
- Mock external dependencies and services
16+
17+
```typescript
18+
import { mount } from '@vue/test-utils';
19+
import { beforeEach, describe, expect, it, vi } from 'vitest';
20+
import YourComponent from '~/components/YourComponent.vue';
21+
22+
// Mock dependencies
23+
vi.mock('~/helpers/someHelper', () => ({
24+
SOME_CONSTANT: 'mocked-value',
25+
}));
26+
27+
describe('YourComponent', () => {
28+
beforeEach(() => {
29+
vi.clearAllMocks();
30+
});
31+
32+
it('renders correctly', () => {
33+
const wrapper = mount(YourComponent, {
34+
global: {
35+
stubs: {
36+
// Stub child components when needed
37+
ChildComponent: true,
38+
},
39+
},
40+
});
41+
42+
// Assertions
43+
expect(wrapper.text()).toContain('Expected content');
44+
});
45+
});
46+
```
47+
48+
### Testing Patterns
49+
- Test component behavior and output, not implementation details
50+
- Verify that the expected elements are rendered
51+
- Test component interactions (clicks, inputs, etc.)
52+
- Check for expected prop handling and event emissions
53+
54+
### Finding Elements
55+
- Use semantic queries like `find('button')` or `find('[data-test="id"]')` but prefer not to use data test ID's
56+
- Find components with `findComponent(ComponentName)`
57+
- Use `findAll` to check for multiple elements
58+
59+
### Assertions
60+
- Assert on rendered text content with `wrapper.text()`
61+
- Assert on element attributes with `element.attributes()`
62+
- Verify element existence with `expect(element.exists()).toBe(true)`
63+
- Check component state through rendered output
64+
65+
### Component Interaction
66+
- Trigger events with `await element.trigger('click')`
67+
- Set input values with `await input.setValue('value')`
68+
- Test emitted events with `wrapper.emitted()`
69+
70+
### Mocking
71+
- Mock external services and API calls
72+
- Use `vi.mock()` for module-level mocks
73+
- Specify return values for component methods with `vi.spyOn()`
74+
- Reset mocks between tests with `vi.clearAllMocks()`
75+
- Frequently used mocks are stored under `web/test/mocks`
76+
77+
### Async Testing
78+
- Use `await nextTick()` for DOM updates
79+
- Use `flushPromises()` for more complex promise chains
80+
- Always await async operations before making assertions
81+
82+
## Store Testing with Pinia
83+
84+
### Setup
85+
- Use `createTestingPinia()` to create a test Pinia instance
86+
- Set `createSpy: vi.fn` to automatically spy on actions
87+
88+
```typescript
89+
import { createTestingPinia } from '@pinia/testing';
90+
import { useYourStore } from '~/store/yourStore';
91+
92+
const pinia = createTestingPinia({
93+
createSpy: vi.fn,
94+
});
95+
const store = useYourStore(pinia);
96+
```
97+
98+
### Testing Actions
99+
- Verify actions are called with the right parameters
100+
- Test action side effects if not stubbed
101+
- Override specific action implementations when needed
102+
103+
```typescript
104+
// Test action calls
105+
store.yourAction(params);
106+
expect(store.yourAction).toHaveBeenCalledWith(params);
107+
108+
// Test with real implementation
109+
const pinia = createTestingPinia({
110+
createSpy: vi.fn,
111+
stubActions: false,
112+
});
113+
```
114+
115+
### Testing State & Getters
116+
- Set initial state for focused testing
117+
- Test computed properties by accessing them directly
118+
- Verify state changes by updating the store
119+

api/ecosystem.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"max_restarts": 10,
1212
"min_uptime": 10000,
1313
"watch": false,
14-
"interpreter": "/usr/local/node/bin/node",
14+
"interpreter": "/usr/local/bin/node",
1515
"ignore_watch": ["node_modules", "src", ".env.*", "myservers.cfg"],
1616
"log_file": "/var/log/graphql-api.log",
1717
"kill_timeout": 10000

api/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export default defineConfig(({ mode }): ViteUserConfig => {
9292
interop: 'auto',
9393
banner: (chunk) => {
9494
if (chunk.fileName === 'main.js' || chunk.fileName === 'cli.js') {
95-
return '#!/usr/local/node/bin/node\n';
95+
return '#!/usr/local/bin/node\n';
9696
}
9797
return '';
9898
},

plugin/plugins/dynamix.unraid.net.plg

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<!-- The archive's filename on the boot drive. Enables reproducible offline installs of the Unraid API. -->
3131
<!ENTITY VENDOR_STORE_FILENAME "">
3232
<!ENTITY TAG "">
33+
<!ENTITY NODE_DIR "/usr/libexec/node">
3334
]>
3435

3536
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;"
@@ -105,9 +106,6 @@ dnscheck "mothership.unraid.net"
105106
[[ "${DNSERR}" == "yes" && "${DNS_SERVER1}" != "8.8.8.8" ]] && echo " Recommend navigating to Settings -> Network Settings and changing your DNS server to 8.8.8.8"
106107
# Note: DNS checks will fail if the network is not available at boot. Cannot exit the install when DNS checks fail.
107108
108-
echo
109-
echo "⚠️ Do not close this window yet"
110-
echo
111109
exit 0
112110
]]>
113111
</INLINE>
@@ -127,6 +125,7 @@ exit 0
127125
<INLINE>
128126
NODE_FILE="&NODEJS_FILENAME;"
129127
VENDOR_ARCHIVE="&VENDOR_STORE_FILENAME;"
128+
NODE_DIR="&NODE_DIR;"
130129
<![CDATA[
131130
# Check if the Node.js archive exists
132131
if [[ ! -f "/boot/config/plugins/dynamix.my.servers/${NODE_FILE}" ]]; then
@@ -140,9 +139,6 @@ exit 0
140139
exit 1
141140
fi
142141
143-
# Define the target directory for Node.js
144-
NODE_DIR="/usr/local/node"
145-
146142
# Create the target directory if it doesn't exist
147143
mkdir -p "${NODE_DIR}" || { echo "Failed to create ${NODE_DIR}"; exit 1; }
148144
@@ -160,6 +156,9 @@ exit 0
160156
find /boot/config/plugins/dynamix.my.servers/ -name "pnpm-store-for-v*.txz" ! -name "${VENDOR_ARCHIVE}" -delete
161157
find /boot/config/plugins/dynamix.my.servers/ -name "node_modules-for-v*.tar.xz" ! -name "${VENDOR_ARCHIVE}" -delete
162158
159+
# Remove the legacy node directory
160+
rm -rf /usr/local/node
161+
163162
echo "Node.js installation successful"
164163
165164
exit 0
@@ -187,10 +186,6 @@ source /usr/local/emhttp/plugins/dynamix.my.servers/scripts/activation_code_remo
187186
echo "Performing cleanup operations..."
188187
/usr/bin/php /usr/local/emhttp/plugins/dynamix.my.servers/scripts/cleanup_operations.php
189188
190-
echo
191-
echo "⚠️ Do not close this window yet"
192-
echo
193-
194189
exit 0
195190
]]>
196191
</INLINE>
@@ -232,10 +227,6 @@ version=
232227
# shellcheck disable=SC1091
233228
source /etc/unraid-version
234229
235-
echo
236-
echo "⚠️ Do not close this window yet"
237-
echo
238-
239230
if [ -e /etc/rc.d/rc.unraid-api ]; then
240231
touch /tmp/restore-files-dynamix-unraid-net
241232
# stop flash backup
@@ -362,6 +353,7 @@ exit 0
362353
TAG="&TAG;" MAINTXZ="&source;.txz"
363354
VENDOR_ARCHIVE="/boot/config/plugins/dynamix.my.servers/&VENDOR_STORE_FILENAME;"
364355
PNPM_BINARY_FILE="&PNPM_BINARY;"
356+
NODE_DIR="&NODE_DIR;"
365357
<![CDATA[
366358
appendTextIfMissing() {
367359
FILE="$1" TEXT="$2"
@@ -394,9 +386,6 @@ EOF
394386
exit 0
395387
fi
396388
397-
echo
398-
echo "⚠️ Do not close this window yet"
399-
echo
400389
401390
# NOTE: any 'exit 1' after this point will result in a broken install
402391
@@ -678,9 +667,6 @@ if [[ -n "$TAG" && "$TAG" != "" ]]; then
678667
sed -i "${sedcmd}" "/usr/local/emhttp/plugins/dynamix.unraid.net/README.md"
679668
fi
680669
681-
echo
682-
echo "⚠️ Do not close this window yet"
683-
echo
684670
685671
# setup env
686672
echo "env=\"production\"">/boot/config/plugins/dynamix.my.servers/env
@@ -766,10 +752,6 @@ if [[ "$webguiManifestTs" -gt "$plgManifestTs" ]]; then
766752
echo "♻️ Reverted to stock web component files"
767753
fi
768754
769-
echo
770-
echo "⚠️ Do not close this window yet"
771-
echo
772-
773755
# Activation and partner setup
774756
# - Must come after the web component timestamp check to avoid potentially targeting the wrong JS during this setup
775757
source /usr/local/emhttp/plugins/dynamix.my.servers/scripts/activation_code_setup
@@ -819,9 +801,9 @@ fi
819801
820802
821803
# Create symlink to unraid-api binary (to allow usage elsewhere)
822-
ln -sf /usr/local/node/bin/node /usr/local/bin/node
823-
ln -sf /usr/local/node/bin/npm /usr/local/bin/npm
824-
ln -sf /usr/local/node/bin/corepack /usr/local/bin/corepack
804+
ln -sf ${NODE_DIR}/bin/node /usr/local/bin/node
805+
ln -sf ${NODE_DIR}/bin/npm /usr/local/bin/npm
806+
ln -sf ${NODE_DIR}/bin/corepack /usr/local/bin/corepack
825807
826808
ln -sf ${unraid_binary_path} /usr/local/sbin/unraid-api
827809
ln -sf ${unraid_binary_path} /usr/bin/unraid-api
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* Auth Component Test Coverage
3+
*/
4+
5+
import { ref } from 'vue';
6+
import { mount } from '@vue/test-utils';
7+
8+
import { describe, expect, it, vi } from 'vitest';
9+
10+
import Auth from '~/components/Auth.ce.vue';
11+
12+
// Define types for our mocks
13+
interface AuthAction {
14+
text: string;
15+
icon: string;
16+
click?: () => void;
17+
disabled?: boolean;
18+
title?: string;
19+
}
20+
21+
interface StateData {
22+
error: boolean;
23+
heading?: string;
24+
message?: string;
25+
}
26+
27+
// Mock vue-i18n
28+
vi.mock('vue-i18n', () => ({
29+
useI18n: () => ({
30+
t: (key: string) => key,
31+
}),
32+
}));
33+
34+
// Mock the useServerStore composable
35+
const mockServerStore = {
36+
authAction: ref<AuthAction | undefined>(undefined),
37+
stateData: ref<StateData>({ error: false }),
38+
};
39+
40+
vi.mock('~/store/server', () => ({
41+
useServerStore: () => mockServerStore,
42+
}));
43+
44+
// Mock pinia's storeToRefs to simply return the store
45+
vi.mock('pinia', () => ({
46+
storeToRefs: (store: unknown) => store,
47+
}));
48+
49+
describe('Auth Component', () => {
50+
it('displays an authentication button when authAction is available', () => {
51+
// Configure auth action
52+
mockServerStore.authAction.value = {
53+
text: 'Sign in to Unraid',
54+
icon: 'key',
55+
click: vi.fn(),
56+
};
57+
mockServerStore.stateData.value = { error: false };
58+
59+
// Mount component
60+
const wrapper = mount(Auth);
61+
62+
// Verify button exists
63+
const button = wrapper.findComponent({ name: 'BrandButton' });
64+
expect(button.exists()).toBe(true);
65+
// Check props passed to button
66+
expect(button.props('text')).toBe('Sign in to Unraid');
67+
expect(button.props('icon')).toBe('key');
68+
});
69+
70+
it('displays error messages when stateData.error is true', () => {
71+
// Configure with error state
72+
mockServerStore.authAction.value = {
73+
text: 'Sign in to Unraid',
74+
icon: 'key',
75+
};
76+
mockServerStore.stateData.value = {
77+
error: true,
78+
heading: 'Error Title',
79+
message: 'Error Message Content',
80+
};
81+
82+
// Mount component
83+
const wrapper = mount(Auth);
84+
85+
// Verify error message is displayed
86+
const errorHeading = wrapper.find('h3');
87+
88+
expect(errorHeading.exists()).toBe(true);
89+
expect(errorHeading.text()).toBe('Error Title');
90+
expect(wrapper.text()).toContain('Error Message Content');
91+
});
92+
93+
it('calls the click handler when button is clicked', async () => {
94+
// Create mock click handler
95+
const clickHandler = vi.fn();
96+
97+
// Configure with click handler
98+
mockServerStore.authAction.value = {
99+
text: 'Sign in to Unraid',
100+
icon: 'key',
101+
click: clickHandler,
102+
};
103+
mockServerStore.stateData.value = { error: false };
104+
105+
// Mount component
106+
const wrapper = mount(Auth);
107+
108+
// Click the button
109+
await wrapper.findComponent({ name: 'BrandButton' }).vm.$emit('click');
110+
111+
// Verify click handler was called
112+
expect(clickHandler).toHaveBeenCalledTimes(1);
113+
});
114+
115+
it('does not render button when authAction is undefined', () => {
116+
// Configure with undefined auth action
117+
mockServerStore.authAction.value = undefined;
118+
mockServerStore.stateData.value = { error: false };
119+
120+
// Mount component
121+
const wrapper = mount(Auth);
122+
123+
// Verify button doesn't exist
124+
const button = wrapper.findComponent({ name: 'BrandButton' });
125+
126+
expect(button.exists()).toBe(false);
127+
});
128+
});

0 commit comments

Comments
 (0)