mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
324 lines
12 KiB
JavaScript
324 lines
12 KiB
JavaScript
// Contract tests for the Pinia main store — key actions and getters.
|
|
//
|
|
// store.js has top-level dependencies (locales/i18n, firebase-init.js,
|
|
// import.meta.env.VITE_*) that don't exist in the Node test environment.
|
|
// The store source already uses explicit `.js` suffixes and reads
|
|
// import.meta.env via optional chaining so it imports cleanly here; we
|
|
// just need to inject minimal localStorage / window / document stubs
|
|
// BEFORE the store import (see below).
|
|
|
|
// ---- globalThis stubs (must be installed before importing store) ----
|
|
globalThis.localStorage = {
|
|
_data: {},
|
|
getItem(k) { return this._data[k] ?? null; },
|
|
setItem(k, v) { this._data[k] = v; },
|
|
removeItem(k) { delete this._data[k]; },
|
|
clear() { this._data = {}; },
|
|
};
|
|
globalThis.window = {
|
|
location: { search: '' },
|
|
addEventListener() {},
|
|
innerWidth: 1024,
|
|
};
|
|
const classListCalls = [];
|
|
globalThis.document = {
|
|
addEventListener() {},
|
|
title: '',
|
|
querySelector() { return null; },
|
|
documentElement: {
|
|
classList: {
|
|
toggle(cls, on) { classListCalls.push({ cls, on }); },
|
|
},
|
|
},
|
|
};
|
|
|
|
import assert from 'node:assert/strict';
|
|
import { describe, it, beforeEach } from 'node:test';
|
|
import { createPinia, setActivePinia } from 'pinia';
|
|
|
|
const { useMainStore } = await import('../frontend/store.js');
|
|
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia());
|
|
classListCalls.length = 0;
|
|
globalThis.localStorage.clear();
|
|
});
|
|
|
|
describe('store — trivial setter actions', () => {
|
|
it('setAlert bundles all five args into state.alert', () => {
|
|
const s = useMainStore();
|
|
s.setAlert(true, 'text-success', 'msg', 'title', 5000);
|
|
assert.deepEqual(s.alert, {
|
|
alertToShow: true,
|
|
alertStyle: 'text-success',
|
|
alertMessage: 'msg',
|
|
alertTitle: 'title',
|
|
alertDuration: 5000,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('store — sheet toggling', () => {
|
|
it('setOpenSheet assigns directly', () => {
|
|
const s = useMainStore();
|
|
s.setOpenSheet('preferences');
|
|
assert.equal(s.openSheet, 'preferences');
|
|
s.setOpenSheet(null);
|
|
assert.equal(s.openSheet, null);
|
|
});
|
|
|
|
it('toggleSheet opens when closed, closes when same name open', () => {
|
|
const s = useMainStore();
|
|
s.toggleSheet('tools');
|
|
assert.equal(s.openSheet, 'tools');
|
|
s.toggleSheet('tools');
|
|
assert.equal(s.openSheet, null);
|
|
});
|
|
|
|
it('toggleSheet switches to new name when different sheet is open', () => {
|
|
const s = useMainStore();
|
|
s.toggleSheet('preferences');
|
|
s.toggleSheet('tools');
|
|
assert.equal(s.openSheet, 'tools');
|
|
});
|
|
});
|
|
|
|
describe('store — dark mode side effect', () => {
|
|
it('setDarkMode flips state AND toggles .dark class on <html>', () => {
|
|
const s = useMainStore();
|
|
s.setDarkMode(true);
|
|
assert.equal(s.isDarkMode, true);
|
|
assert.deepEqual(classListCalls.at(-1), { cls: 'dark', on: true });
|
|
s.setDarkMode(false);
|
|
assert.deepEqual(classListCalls.at(-1), { cls: 'dark', on: false });
|
|
});
|
|
});
|
|
|
|
describe('store — IPDBs / allIPs', () => {
|
|
|
|
it('updateAllIPs dedupes by ip and back-fills a missing country', () => {
|
|
const s = useMainStore();
|
|
s.updateAllIPs([{ ip: '1.1.1.1', country: 'US' }, { ip: '2.2.2.2', country: '' }]);
|
|
s.updateAllIPs([{ ip: '2.2.2.2', country: 'DE' }, { ip: '3.3.3.3', country: 'JP' }]);
|
|
const byIp = Object.fromEntries(s.allIPs.map((e) => [e.ip, e.country]));
|
|
assert.deepEqual(byIp, { '1.1.1.1': 'US', '2.2.2.2': 'DE', '3.3.3.3': 'JP' });
|
|
});
|
|
|
|
it('updateAllIPs tolerates bare-string entries', () => {
|
|
const s = useMainStore();
|
|
s.updateAllIPs(['9.9.9.9']);
|
|
assert.deepEqual(s.allIPs, [{ ip: '9.9.9.9', country: '', location: '', asn: '', org: '' }]);
|
|
});
|
|
|
|
it('updateAllIPs back-fills location / asn / org left empty by an earlier source', () => {
|
|
const s = useMainStore();
|
|
s.updateAllIPs([{ ip: '1.1.1.1', country: 'US' }]);
|
|
s.updateAllIPs([{ ip: '1.1.1.1', location: 'United States · LA', asn: 'AS13335', org: 'Cloudflare' }]);
|
|
// A later entry must not overwrite a field that already has a value.
|
|
s.updateAllIPs([{ ip: '1.1.1.1', asn: 'AS9999' }]);
|
|
assert.deepEqual(s.allIPs, [{
|
|
ip: '1.1.1.1', country: 'US', location: 'United States · LA', asn: 'AS13335', org: 'Cloudflare',
|
|
}]);
|
|
});
|
|
});
|
|
|
|
describe('store — trigger* actions', () => {
|
|
it('setTriggerRemoteUserInfo only sets when truthy (guard against reset loops)', () => {
|
|
const s = useMainStore();
|
|
s.setTriggerRemoteUserInfo(true);
|
|
assert.equal(s.triggerRemoteUserInfo, true);
|
|
// Passing false should NOT flip it back in current implementation
|
|
s.setTriggerRemoteUserInfo(false);
|
|
assert.equal(s.triggerRemoteUserInfo, true);
|
|
});
|
|
|
|
it('setTriggerUpdateAchievements writes flag + achievement name', () => {
|
|
const s = useMainStore();
|
|
s.setTriggerUpdateAchievements('CleverTrickery');
|
|
assert.equal(s.triggerUpdateAchievements, true);
|
|
assert.equal(s.achievementToUpdate, 'CleverTrickery');
|
|
});
|
|
});
|
|
|
|
describe('store — preferences', () => {
|
|
it('setPreferences persists to localStorage', () => {
|
|
const s = useMainStore();
|
|
s.setPreferences({ lang: 'zh', simpleMode: false });
|
|
assert.deepEqual(s.userPreferences, { lang: 'zh', simpleMode: false });
|
|
const fromStorage = JSON.parse(globalThis.localStorage.getItem('userPreferences_v7'));
|
|
assert.deepEqual(fromStorage, { lang: 'zh', simpleMode: false });
|
|
});
|
|
|
|
it('updatePreference mutates a single key AND persists', () => {
|
|
const s = useMainStore();
|
|
s.setPreferences({ lang: 'en', autoRunConnectivity: true });
|
|
s.updatePreference('autoRunConnectivity', false);
|
|
assert.equal(s.userPreferences.autoRunConnectivity, false);
|
|
const fromStorage = JSON.parse(globalThis.localStorage.getItem('userPreferences_v7'));
|
|
assert.equal(fromStorage.autoRunConnectivity, false);
|
|
});
|
|
|
|
it('loadPreferences seeds defaults when nothing stored', () => {
|
|
const s = useMainStore();
|
|
s.loadPreferences();
|
|
// Defaults come from createDefaultPreferences(); we don't pin the whole shape,
|
|
// just assert the result is a non-empty object and gets persisted.
|
|
assert.equal(typeof s.userPreferences, 'object');
|
|
assert.ok(Object.keys(s.userPreferences).length > 0);
|
|
assert.ok(globalThis.localStorage.getItem('userPreferences_v7'));
|
|
});
|
|
|
|
it('loadPreferences merges stored over defaults (stored keys win)', () => {
|
|
globalThis.localStorage.setItem('userPreferences_v7', JSON.stringify({ lang: 'zh' }));
|
|
const s = useMainStore();
|
|
s.loadPreferences();
|
|
assert.equal(s.userPreferences.lang, 'zh', 'stored lang wins');
|
|
// A default key still present (autoRunConnectivity exists in defaults)
|
|
assert.ok('autoRunConnectivity' in s.userPreferences, 'default keys fill in missing slots');
|
|
});
|
|
|
|
it('loadPreferences ignores legacy pref keys entirely', () => {
|
|
globalThis.localStorage.setItem(
|
|
'userPreferences_v6',
|
|
JSON.stringify({ lang: 'fr', autoStart: false }),
|
|
);
|
|
const s = useMainStore();
|
|
s.loadPreferences();
|
|
// Legacy migration was retired: v6 content must not leak into the store,
|
|
// and the stale key is left untouched (nothing reads it anymore).
|
|
assert.equal(s.userPreferences.lang, 'auto', 'legacy lang does not carry over');
|
|
assert.equal(s.userPreferences.autoRunConnectivity, true, 'defaults win');
|
|
assert.ok(!('autoStart' in s.userPreferences));
|
|
});
|
|
});
|
|
|
|
describe('store — getters', () => {
|
|
it('allHasLoaded is false when any loading flag is false', () => {
|
|
const s = useMainStore();
|
|
assert.equal(s.allHasLoaded, false);
|
|
});
|
|
|
|
it('allHasLoaded flips true when every flag is true', () => {
|
|
const s = useMainStore();
|
|
for (const key of Object.keys(s.loadingStatus)) s.setLoadingStatus(key, true);
|
|
assert.equal(s.allHasLoaded, true);
|
|
});
|
|
|
|
it('activeSources returns only enabled databases', () => {
|
|
const s = useMainStore();
|
|
const firstId = s.ipDBs[0].id;
|
|
// Availability is config-derived state; flip it the way fetchConfigs does.
|
|
s.ipDBs[0].enabled = false;
|
|
const active = s.activeSources;
|
|
assert.ok(active.every((db) => db.enabled));
|
|
assert.ok(!active.find((db) => db.id === firstId));
|
|
});
|
|
|
|
it('curlDomainsHadSet is false when env not set', () => {
|
|
const s = useMainStore();
|
|
// In Node tests we don't inject VITE_CURL_* → curl.* is undefined → getter is falsy
|
|
assert.equal(Boolean(s.curlDomainsHadSet), false);
|
|
});
|
|
});
|
|
|
|
describe('store — getDbUrl delegates to buildDbUrl', () => {
|
|
it('returns a URL built from the matching db template', () => {
|
|
const s = useMainStore();
|
|
const db = s.ipDBs[0];
|
|
const url = s.getDbUrl(db.id, '1.1.1.1', 'en');
|
|
// db.url is a template like /api/x?ip={{ip}}&lang={{lang}} — verify substitution
|
|
assert.match(url, /1\.1\.1\.1/);
|
|
assert.match(url, /en/);
|
|
});
|
|
|
|
it('returns a falsy value when id is unknown', () => {
|
|
const s = useMainStore();
|
|
const result = s.getDbUrl(9999, '1.1.1.1', 'en');
|
|
// buildDbUrl returns undefined/'' for no-match; we only guarantee falsy
|
|
assert.ok(!result, `expected falsy result for unknown id, got ${result}`);
|
|
});
|
|
});
|
|
|
|
describe('store — linkedProviders', () => {
|
|
const signedInWith = (store, ...providerIds) => {
|
|
store.isSignedIn = true;
|
|
store.user = { providerData: providerIds.map((providerId) => ({ providerId })) };
|
|
};
|
|
|
|
it('is empty while signed out', () => {
|
|
const s = useMainStore();
|
|
s.user = null;
|
|
assert.deepEqual(s.linkedProviders, []);
|
|
});
|
|
|
|
it('names each provider and the icon the menu renders', () => {
|
|
const s = useMainStore();
|
|
signedInWith(s, 'google.com');
|
|
assert.deepEqual(s.linkedProviders, [
|
|
{ providerId: 'google.com', label: 'Google', icon: 'ri:google-line' },
|
|
]);
|
|
});
|
|
|
|
it('keeps Firebase order when several are present', () => {
|
|
const s = useMainStore();
|
|
signedInWith(s, 'github.com', 'google.com');
|
|
assert.deepEqual(s.linkedProviders.map((entry) => entry.label), ['GitHub', 'Google']);
|
|
});
|
|
|
|
it('falls back to the raw id for a provider the app does not offer', () => {
|
|
const s = useMainStore();
|
|
signedInWith(s, 'password');
|
|
// Shown rather than hidden: an account signing in some other way should
|
|
// not read as having no sign-in method at all.
|
|
assert.deepEqual(s.linkedProviders, [
|
|
{ providerId: 'password', label: 'password', icon: null },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('store — handleSignInError', () => {
|
|
const google = { label: 'Google', other: 'GitHub' };
|
|
const github = { label: 'GitHub', other: 'Google' };
|
|
// i18n has no messages loaded here, so t() returns the key itself — which
|
|
// makes these tests about WHICH message each code picks.
|
|
const fail = (store, code, descriptor) => {
|
|
store.setAlert(false, '', '', '', 0);
|
|
store.handleSignInError({ code }, descriptor);
|
|
return store.alert;
|
|
};
|
|
|
|
it('stays silent when the visitor just closes the popup', () => {
|
|
const s = useMainStore();
|
|
for (const code of ['auth/popup-closed-by-user', 'auth/cancelled-popup-request', 'auth/user-cancelled']) {
|
|
assert.equal(fail(s, code, github).alertToShow, false, code);
|
|
}
|
|
});
|
|
|
|
it('warns and points at the other provider when the email is taken', () => {
|
|
const s = useMainStore();
|
|
for (const code of [
|
|
'auth/account-exists-with-different-credential',
|
|
'auth/email-already-in-use',
|
|
'auth/credential-already-in-use',
|
|
]) {
|
|
const alert = fail(s, code, github);
|
|
assert.equal(alert.alertMessage, 'alert.SignInEmailTakenMessage', code);
|
|
assert.equal(alert.alertStyle, 'text-warning', code);
|
|
}
|
|
});
|
|
|
|
it('still surfaces unexpected failures as errors', () => {
|
|
const s = useMainStore();
|
|
const unknown = fail(s, 'auth/network-request-failed', google);
|
|
assert.equal(unknown.alertStyle, 'text-danger');
|
|
assert.ok(unknown.alertToShow);
|
|
});
|
|
|
|
it('survives an error object with no code at all', () => {
|
|
const s = useMainStore();
|
|
s.setAlert(false, '', '', '', 0);
|
|
s.handleSignInError(new Error('boom'), google);
|
|
assert.equal(s.alert.alertStyle, 'text-danger');
|
|
});
|
|
});
|