feat(KAN-70): integrate native workspace and update settings
CI/CD / preconditions (pull_request) Successful in 1s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m2s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m38s
CI/CD / test_and_check_app (pull_request) Successful in 6m47s
CI/CD / Publish immutable image (pull_request) Skipped
CI/CD / preconditions (pull_request) Successful in 1s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m2s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m38s
CI/CD / test_and_check_app (pull_request) Successful in 6m47s
CI/CD / Publish immutable image (pull_request) Skipped
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { createServer } from 'vite';
|
||||
import { createSSRApp, h } from 'vue';
|
||||
import { renderToString } from 'vue/server-renderer';
|
||||
import { createInertiaApp } from '@inertiajs/vue3';
|
||||
|
||||
const server = await createServer({
|
||||
configFile: false,
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('../../resources/js', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: { middlewareMode: true, hmr: false, watch: null },
|
||||
appType: 'custom',
|
||||
});
|
||||
try {
|
||||
const { createLocalization } = await server.ssrLoadModule(
|
||||
'/resources/js/lib/localization.ts',
|
||||
);
|
||||
const { createAppearance } = await server.ssrLoadModule(
|
||||
'/resources/js/composables/useAppearance.ts',
|
||||
);
|
||||
const { initializeNativeApp, useNativeApp, registerNativeRecovery } =
|
||||
await server.ssrLoadModule('/resources/js/composables/useNativeApp.ts');
|
||||
const { isNativeAppState } = await server.ssrLoadModule(
|
||||
'/resources/js/lib/native-app.ts',
|
||||
);
|
||||
const App = (
|
||||
await server.ssrLoadModule('/resources/js/pages/settings/App.vue')
|
||||
).default;
|
||||
const Appearance = (
|
||||
await server.ssrLoadModule(
|
||||
'/resources/js/pages/settings/Appearance.vue',
|
||||
)
|
||||
).default;
|
||||
const render = async (signedIn = false, component = App) => {
|
||||
const result = await createInertiaApp({
|
||||
page: {
|
||||
component: 'settings/App',
|
||||
url: '/settings/app',
|
||||
version: null,
|
||||
clearHistory: false,
|
||||
encryptHistory: false,
|
||||
props: {
|
||||
name: 'KanbanGit',
|
||||
errors: {},
|
||||
sidebarOpen: true,
|
||||
appearance: 'system',
|
||||
auth: {
|
||||
user: signedIn
|
||||
? { name: 'Test', email: 'test@example.test' }
|
||||
: null,
|
||||
permissions: {
|
||||
globalAdmin: false,
|
||||
projectCreate: false,
|
||||
canManageTeams: false,
|
||||
},
|
||||
},
|
||||
navigation: { project: null },
|
||||
},
|
||||
},
|
||||
resolve: () => component,
|
||||
render: renderToString,
|
||||
setup: ({ App, props, plugin }) =>
|
||||
createSSRApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.use(createLocalization())
|
||||
.use(createAppearance('system')),
|
||||
});
|
||||
return result.body;
|
||||
};
|
||||
await initializeNativeApp();
|
||||
assert.match(await render(), /compatible KanbanGit app/);
|
||||
assert.doesNotMatch(
|
||||
await render(true, Appearance),
|
||||
/href="\/settings\/app"/,
|
||||
);
|
||||
let calls = [];
|
||||
let current = {
|
||||
apiVersion: 1,
|
||||
platform: 'ios',
|
||||
version: '1.0.0',
|
||||
workspace: 'https://example.test/app/',
|
||||
capabilities: ['workspace'],
|
||||
};
|
||||
const bridge = {
|
||||
version: 2,
|
||||
request: async (action, payload) => {
|
||||
calls.push([action, payload]);
|
||||
return structuredClone(current);
|
||||
},
|
||||
};
|
||||
globalThis.window = { kanbanNative: bridge };
|
||||
await initializeNativeApp();
|
||||
assert.equal(calls.length, 0);
|
||||
bridge.version = 1;
|
||||
current.apiVersion = 2;
|
||||
await initializeNativeApp();
|
||||
assert.equal(useNativeApp().state.value, null);
|
||||
current.apiVersion = 1;
|
||||
await initializeNativeApp();
|
||||
assert.equal(calls.at(-1)[0], 'settingsReady');
|
||||
delete globalThis.window;
|
||||
let html = await render();
|
||||
assert.match(html, /value="https:\/\/example.test\/app\/"/);
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/Check for updates now|Install update|native-update-interval/,
|
||||
);
|
||||
assert.match(await render(true, Appearance), /href="\/settings\/app"/);
|
||||
current = {
|
||||
...current,
|
||||
platform: 'macos',
|
||||
capabilities: ['workspace', 'updates'],
|
||||
updates: {
|
||||
phase: 'available',
|
||||
currentVersion: '1.0.0',
|
||||
build: '1',
|
||||
version: '1.1.0',
|
||||
notes: '<script>bad()</script>',
|
||||
downloaded: 0,
|
||||
total: null,
|
||||
error: null,
|
||||
preferences: { automatic: true, intervalHours: 6 },
|
||||
},
|
||||
};
|
||||
globalThis.window = { kanbanNative: bridge };
|
||||
await useNativeApp().poll();
|
||||
delete globalThis.window;
|
||||
html = await render(true);
|
||||
assert.match(html, /Check for updates now/);
|
||||
assert.match(html, /Install update/);
|
||||
assert.match(html, /<script>bad/);
|
||||
assert.doesNotMatch(html, /<script>bad/);
|
||||
assert.match(html, /id="native-update-interval"[^>]*value="6"/);
|
||||
for (const phase of [
|
||||
'checking',
|
||||
'downloading',
|
||||
'verifying',
|
||||
'installing',
|
||||
'restarting',
|
||||
'error',
|
||||
'current',
|
||||
]) {
|
||||
current.updates.phase = phase;
|
||||
current.updates.downloaded = 40;
|
||||
current.updates.total = 100;
|
||||
globalThis.window = { kanbanNative: bridge };
|
||||
await useNativeApp().poll();
|
||||
if (
|
||||
[
|
||||
'checking',
|
||||
'downloading',
|
||||
'verifying',
|
||||
'installing',
|
||||
'restarting',
|
||||
].includes(phase)
|
||||
) {
|
||||
const before = calls.length;
|
||||
assert.equal(await useNativeApp().act('checkForUpdates'), false);
|
||||
assert.equal(calls.length, before);
|
||||
}
|
||||
delete globalThis.window;
|
||||
html = await render();
|
||||
if (phase === 'downloading') {
|
||||
assert.match(html, /40%/);
|
||||
}
|
||||
}
|
||||
for (const invalid of [
|
||||
null,
|
||||
{},
|
||||
{ ...current, apiVersion: 2 },
|
||||
{ ...current, platform: 'unsupported' },
|
||||
{ ...current, updates: { ...current.updates, phase: 'madeUp' } },
|
||||
]) {
|
||||
assert.equal(isNativeAppState(invalid), false);
|
||||
}
|
||||
globalThis.window = { kanbanNative: bridge };
|
||||
calls = [];
|
||||
const listeners = {};
|
||||
globalThis.window.addEventListener = (name, callback) => {
|
||||
listeners[name] = callback;
|
||||
};
|
||||
globalThis.document = {
|
||||
addEventListener: (name, callback) => {
|
||||
listeners[name] = callback;
|
||||
},
|
||||
};
|
||||
registerNativeRecovery();
|
||||
for (const name of ['offline', 'inertia:exception', 'inertia:invalid']) {
|
||||
listeners[name]({ detail: { response: { status: 503 } } });
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(calls.at(-1)[0], 'openSettings');
|
||||
}
|
||||
const count = calls.length;
|
||||
listeners['inertia:invalid']({ detail: { response: { status: 200 } } });
|
||||
assert.equal(calls.length, count);
|
||||
delete globalThis.document;
|
||||
calls = [];
|
||||
await useNativeApp().act('setUpdatePreferences', {
|
||||
automatic: false,
|
||||
intervalHours: 24,
|
||||
});
|
||||
assert.deepEqual(calls[0], [
|
||||
'setUpdatePreferences',
|
||||
{ automatic: false, intervalHours: 24 },
|
||||
]);
|
||||
await useNativeApp().act('setWorkspace', { server: 'https://new.test/' });
|
||||
assert.deepEqual(calls.at(-1), [
|
||||
'setWorkspace',
|
||||
{ server: 'https://new.test/' },
|
||||
]);
|
||||
let resolve;
|
||||
bridge.request = () =>
|
||||
new Promise((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
const first = useNativeApp().act('setWorkspace', {
|
||||
server: 'https://new.test/',
|
||||
});
|
||||
assert.equal(
|
||||
await useNativeApp().act('setWorkspace', {
|
||||
server: 'https://duplicate.test/',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
resolve(null);
|
||||
await first;
|
||||
bridge.request = async () => {
|
||||
throw new Error('invalidServer');
|
||||
};
|
||||
assert.equal(
|
||||
await useNativeApp().act('setWorkspace', {
|
||||
server: 'http://invalid.test',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
delete globalThis.window;
|
||||
assert.match(await render(), /Enter a valid HTTPS/);
|
||||
console.log('Native app settings checks passed.');
|
||||
} finally {
|
||||
delete globalThis.window;
|
||||
await server.close();
|
||||
}
|
||||
Reference in New Issue
Block a user