fix some
This commit is contained in:
1 parent
be18543b04
commit
aa3872fbb1
20 files changed
+2079
-230
No files matched your search
@@ -0,0 +1,185 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const ts = require('typescript');
|
||||
const realAxios = require('axios');
|
||||
|
||||
// Execute the actual TypeScript modules against Axios adapters and a browser stub.
|
||||
// No additional runtime/test dependencies, no network, no production API.
|
||||
function harness() {
|
||||
const local = new Map(); const session = new Map(); const redirects = [];
|
||||
const storage = map => ({ getItem: key => map.get(key) ?? null, setItem: (key, value) => map.set(key, String(value)), removeItem: key => map.delete(key) });
|
||||
const window = { localStorage: storage(local), sessionStorage: storage(session), location: { pathname: '/shop', replace: value => redirects.push(value) } };
|
||||
const document = { cookie: '' };
|
||||
const axios = realAxios.create(); axios.create = realAxios.create;
|
||||
axios.isAxiosError = realAxios.isAxiosError;
|
||||
const cache = {};
|
||||
const hooks = []; let cursor = 0;
|
||||
const react = { ...require('react'), useState: initial => {
|
||||
const index = cursor++;
|
||||
if (!(index in hooks)) hooks[index] = initial;
|
||||
return [hooks[index], value => { hooks[index] = typeof value === 'function' ? value(hooks[index]) : value; }];
|
||||
}, useEffect: effect => effect() };
|
||||
const dispatches = [];
|
||||
function render(component) { cursor = 0; return component(); }
|
||||
function load(filename) {
|
||||
const full = path.resolve(filename);
|
||||
if (cache[full]) return cache[full].exports;
|
||||
const module = { exports: {} }; cache[full] = module;
|
||||
const source = ts.transpileModule(fs.readFileSync(full, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true, jsx: ts.JsxEmit.ReactJSX } }).outputText;
|
||||
const requireStub = name => {
|
||||
if (name === 'axios') return axios;
|
||||
if (name === 'react') return react;
|
||||
if (name === 'react/jsx-runtime') return require(name);
|
||||
if (name === 'next/navigation') return { useRouter: () => ({ push: value => redirects.push(value), replace: value => redirects.push(value) }) };
|
||||
if (name === 'react-redux') return { useDispatch: () => action => dispatches.push(action), useSelector: select => select({ mobileopt: { mobile: 9123456789 } }) };
|
||||
if (name.includes('mobileoptSlice')) return { addMobile: payload => ({ payload }) };
|
||||
const base = path.join('src', name.slice(2));
|
||||
return load(base + (fs.existsSync(base + '.ts') ? '.ts' : '.tsx'));
|
||||
};
|
||||
vm.runInNewContext('(function(require, module, exports){' + source + '\n})', { window, document, process, Error, Promise, URLSearchParams, console })(requireStub, module, module.exports);
|
||||
return module.exports;
|
||||
}
|
||||
const auth = load('src/lib/auth.ts');
|
||||
const instance = load('src/axios/axiosInstance.ts').default;
|
||||
return { auth, instance, axios, local, session, redirects, document, load, render, dispatches };
|
||||
}
|
||||
function failure(config, status, data = { detail: 'Error' }) {
|
||||
return new realAxios.AxiosError('Request failed', 'ERR_BAD_RESPONSE', config, null, { data, status, statusText: 'Failed', headers: {}, config });
|
||||
}
|
||||
function response(config, data, status = 200) { return { config, data, status, statusText: 'OK', headers: {} }; }
|
||||
|
||||
test('stores valid tokens and rejects malformed success responses', () => {
|
||||
const h = harness();
|
||||
h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' });
|
||||
assert.equal(h.auth.getAccessToken(), 'access.old');
|
||||
assert.equal(h.auth.getRefreshToken(), 'refresh.old');
|
||||
assert.match(h.document.cookie, /isAuth=true/);
|
||||
assert.throws(() => h.auth.saveTokens({ access: undefined, refresh: 'bad' }));
|
||||
});
|
||||
test('request with no token never sends Bearer null or undefined', async () => {
|
||||
const h = harness();
|
||||
h.instance.defaults.adapter = config => { assert.equal(config.headers.Authorization, undefined); return Promise.resolve(response(config, {})); };
|
||||
await h.instance.get('/public/');
|
||||
});
|
||||
test('400, 403, 500 and network failures always reject and preserve session', async () => {
|
||||
const h = harness(); h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' });
|
||||
for (const status of [400, 403, 500, null]) {
|
||||
h.instance.defaults.adapter = config => Promise.reject(status ? failure(config, status) : new realAxios.AxiosError('Offline', 'ERR_NETWORK', config));
|
||||
await assert.rejects(h.instance.get('/customer/v2/create/'));
|
||||
assert.equal(h.auth.getAccessToken(), 'access.old');
|
||||
assert.equal(h.redirects.length, 0);
|
||||
}
|
||||
});
|
||||
test('simultaneous 401 requests perform one refresh and both retry', async () => {
|
||||
const h = harness(); h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' });
|
||||
let calls = 0;
|
||||
h.axios.defaults.adapter = async config => { calls++; await new Promise(resolve => setImmediate(resolve)); return response(config, { access: 'access.new', refresh: 'refresh.new' }); };
|
||||
h.instance.defaults.adapter = config => config.headers.Authorization === 'Bearer access.old' ? Promise.reject(failure(config, 401)) : Promise.resolve(response(config, { ok: true }));
|
||||
const result = await Promise.all([h.instance.get('/first/'), h.instance.get('/second/')]);
|
||||
assert.equal(calls, 1); assert.equal(result.length, 2); assert.equal(h.auth.getAccessToken(), 'access.new');
|
||||
});
|
||||
test('revoked refresh clears tokens, cookie and redirects to login', async () => {
|
||||
const h = harness(); h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' });
|
||||
h.instance.defaults.adapter = config => Promise.reject(failure(config, 401));
|
||||
h.axios.defaults.adapter = config => Promise.reject(failure(config, 401));
|
||||
await assert.rejects(h.instance.get('/cuser/info/'));
|
||||
assert.equal(h.auth.getAccessToken(), null); assert.equal(h.auth.getRefreshToken(), null);
|
||||
assert.equal(h.redirects[0], '/login?reason=session'); assert.match(h.document.cookie, /max-age=0/);
|
||||
});
|
||||
test('401 without refresh logs out and rejects', async () => {
|
||||
const h = harness();
|
||||
h.instance.defaults.adapter = config => Promise.reject(failure(config, 401));
|
||||
await assert.rejects(h.instance.get('/cuser/info/'));
|
||||
assert.equal(h.redirects[0], '/login?reason=session');
|
||||
});
|
||||
test('network failure while refreshing preserves recoverable session', async () => {
|
||||
const h = harness(); h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' });
|
||||
h.instance.defaults.adapter = config => Promise.reject(failure(config, 401));
|
||||
h.axios.defaults.adapter = config => Promise.reject(new realAxios.AxiosError('Offline', 'ERR_NETWORK', config));
|
||||
await assert.rejects(h.instance.get('/cuser/info/'));
|
||||
assert.equal(h.auth.getRefreshToken(), 'refresh.old'); assert.equal(h.redirects.length, 0);
|
||||
});
|
||||
test('malformed refresh success logs out instead of storing undefined', async () => {
|
||||
const h = harness(); h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' });
|
||||
h.instance.defaults.adapter = config => Promise.reject(failure(config, 401));
|
||||
h.axios.defaults.adapter = config => Promise.resolve(response(config, { error: 'Invalid' }));
|
||||
await assert.rejects(h.instance.get('/cuser/info/'));
|
||||
assert.equal(h.auth.getAccessToken(), null);
|
||||
});
|
||||
test('retry 401 does not refresh forever', async () => {
|
||||
const h = harness(); h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' }); let calls = 0;
|
||||
h.instance.defaults.adapter = config => Promise.reject(failure(config, 401));
|
||||
h.axios.defaults.adapter = config => { calls++; return Promise.resolve(response(config, { access: 'access.new', refresh: 'refresh.new' })); };
|
||||
await assert.rejects(h.instance.get('/cuser/info/')); assert.equal(calls, 1); assert.equal(h.auth.getAccessToken(), null);
|
||||
});
|
||||
test('public auth 401 displays error without triggering token refresh', async () => {
|
||||
const h = harness(); let calls = 0;
|
||||
h.instance.defaults.adapter = config => Promise.reject(failure(config, 401));
|
||||
h.axios.defaults.adapter = config => { calls++; return Promise.resolve(response(config, {})); };
|
||||
await assert.rejects(h.instance.post('/auth/token/', {}));
|
||||
assert.equal(calls, 0); assert.equal(h.redirects.length, 0);
|
||||
});
|
||||
test('logout revokes original refresh token and clears all local identity', async () => {
|
||||
const h = harness(); h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' }); h.local.set('nameFamily', 'Previous');
|
||||
h.axios.defaults.adapter = config => { assert.match(config.url, /auth\/logout/); assert.equal(JSON.parse(config.data).refresh, 'refresh.old'); return Promise.resolve(response(config, null, 204)); };
|
||||
await h.auth.logout(); assert.equal(h.local.size, 0); assert.equal(h.redirects[0], '/login');
|
||||
});
|
||||
test('late refresh response cannot restore a logged-out session', async () => {
|
||||
const h = harness(); h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' }); let resolveRefresh;
|
||||
h.instance.defaults.adapter = config => Promise.reject(failure(config, 401));
|
||||
h.axios.defaults.adapter = config => new Promise(resolve => { resolveRefresh = () => resolve(response(config, { access: 'access.new', refresh: 'refresh.new' })); });
|
||||
const pending = h.instance.get('/cuser/info/');
|
||||
await new Promise(resolve => setImmediate(resolve)); h.auth.clearSession(); resolveRefresh();
|
||||
await assert.rejects(pending); assert.equal(h.auth.getAccessToken(), null);
|
||||
});
|
||||
test('API messages including duplicate mobile and SMS failure are preserved', () => {
|
||||
const h = harness();
|
||||
const config = {};
|
||||
assert.equal(h.auth.apiErrorMessage(failure(config, 400, { mobile: ['شماره تکراری است'] })), 'شماره تکراری است');
|
||||
assert.equal(h.auth.apiErrorMessage(failure(config, 503, { detail: 'ارسال پیامک انجام نشد' })), 'ارسال پیامک انجام نشد');
|
||||
});
|
||||
|
||||
function elements(tree, predicate) {
|
||||
if (!tree || typeof tree !== 'object') return [];
|
||||
if (Array.isArray(tree)) return tree.flatMap(item => elements(item, predicate));
|
||||
return [...(predicate(tree) ? [tree] : []), ...elements(tree.props?.children, predicate)];
|
||||
}
|
||||
function textContent(tree) {
|
||||
if (typeof tree === 'string' || typeof tree === 'number') return String(tree);
|
||||
if (Array.isArray(tree)) return tree.map(textContent).join('');
|
||||
return tree?.props ? textContent(tree.props.children) : '';
|
||||
}
|
||||
test('login page renders API errors and re-enables submit', async () => {
|
||||
const h = harness(); const component = h.load('src/app/(login_register)/login/page.tsx').default;
|
||||
h.axios.defaults.adapter = config => Promise.reject(failure(config, 503, { detail: 'ارسال پیامک انجام نشد' }));
|
||||
const form = elements(h.render(component), item => item.type === 'form')[0];
|
||||
await form.props.onSubmit({ preventDefault() {} });
|
||||
const tree = h.render(component);
|
||||
const alerts = elements(tree, item => item.props?.role === 'alert');
|
||||
assert.ok(alerts.some(item => textContent(item).includes('ارسال پیامک انجام نشد')));
|
||||
assert.ok(elements(tree, item => item.type === 'button').some(item => !item.props.disabled));
|
||||
assert.equal(h.redirects.length, 0);
|
||||
});
|
||||
|
||||
test('multi-role login renders role choices without automatically entering first workspace', async () => {
|
||||
const h = harness(); const component = h.load('src/app/(login_register)/login/page.tsx').default;
|
||||
h.axios.defaults.adapter = config => Promise.resolve(response(config, [1,2].map(id => ({ uuid: String(id), cuser_role: { sys_ws: { name: 'Workspace'+id }, role_type: { role_name: 'Operator' } } }))));
|
||||
await elements(h.render(component), item => item.type === 'form')[0].props.onSubmit({ preventDefault() {} });
|
||||
const tree = h.render(component);
|
||||
assert.equal(elements(tree, item => item.type === 'button' && item.props.type === 'button').length, 2);
|
||||
assert.equal(h.auth.getAccessToken(), null); assert.equal(h.redirects.length, 0);
|
||||
});
|
||||
|
||||
test('a stale retry failure does not clear a newer session from another tab', async () => {
|
||||
const h = harness(); h.auth.saveTokens({ access: 'access.old', refresh: 'refresh.old' });
|
||||
h.instance.defaults.adapter = config => {
|
||||
if (config.headers.Authorization === 'Bearer access.new') h.auth.saveTokens({ access: 'access.other-tab', refresh: 'refresh.other-tab' });
|
||||
return Promise.reject(failure(config, 401));
|
||||
};
|
||||
h.axios.defaults.adapter = config => Promise.resolve(response(config, { access: 'access.new', refresh: 'refresh.new' }));
|
||||
await assert.rejects(h.instance.get('/cuser/info/'));
|
||||
assert.equal(h.auth.getAccessToken(), 'access.other-tab'); assert.equal(h.redirects.length, 0);
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
const test = require('node:test'), assert = require('node:assert/strict');
|
||||
const fs = require('node:fs'), path = require('node:path'), vm = require('node:vm');
|
||||
const ts = require('typescript'), axios = require('axios');
|
||||
function harness() {
|
||||
const cache = {}, state = [], queries = new Map(), requests = [], invalidations = [], downloads = [];
|
||||
let cursor = 0;
|
||||
const local = new Map([['access', 'token']]);
|
||||
const window = { localStorage: { getItem: k => local.get(k) ?? null, setItem: (k, v) => local.set(k,v), removeItem: k => local.delete(k) }, sessionStorage: { removeItem() {} }, location: { pathname: '/products', replace() {} }, confirm: () => true };
|
||||
const document = { cookie: '', body: { appendChild() {} }, createElement: () => ({ click() { downloads.push(this.download); }, remove() {} }) };
|
||||
class FormDataStub { get [Symbol.toStringTag]() { return 'FormData'; } [Symbol.iterator]() { return this.values.entries(); } constructor(values = {}) { this.values = new Map(Object.entries(values)); } get(k) { return this.values.get(k) ?? null; } set(k,v) { this.values.set(k,v); } append(k,v) { this.values.set(k,v); } entries() { return this.values.entries(); } }
|
||||
const react = { ...require('react'), useState(initial) { const i = cursor++; if (!(i in state)) state[i] = initial; return [state[i], v => state[i] = typeof v === 'function' ? v(state[i]) : v]; } };
|
||||
const query = { useQuery: o => queries.get(o.queryKey[0]) || { isLoading: false, isError: false, refetch() {} }, useQueryClient: () => ({ invalidateQueries: async o => invalidations.push(o.queryKey[0]) }) };
|
||||
function load(filename) {
|
||||
const full = path.resolve(filename); if (cache[full]) return cache[full].exports;
|
||||
const module = { exports: {} }; cache[full] = module;
|
||||
const source = ts.transpileModule(fs.readFileSync(full, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true, jsx: ts.JsxEmit.ReactJSX } }).outputText;
|
||||
const resolve = name => {
|
||||
if (name === 'axios') return axios;
|
||||
if (name === 'react') return react;
|
||||
if (name === '@tanstack/react-query') return query;
|
||||
if (name === 'react/jsx-runtime' || name.startsWith('react-icons/')) return require(name);
|
||||
if (name === 'next/link') return { __esModule: true, default: 'a' };
|
||||
const base = name.startsWith('@/') ? path.resolve('src', name.slice(2)) : path.resolve(path.dirname(full), name);
|
||||
return load(base + (fs.existsSync(base+'.ts') ? '.ts' : '.tsx'));
|
||||
};
|
||||
vm.runInNewContext('(function(require,module,exports){'+source+'\n})', { window, document, process, console, Error, BigInt, Set, Map, Blob, URL: { createObjectURL: () => 'blob:test', revokeObjectURL() {} }, FormData: FormDataStub, setTimeout: cb => cb() })(resolve, module, module.exports);
|
||||
return module.exports;
|
||||
}
|
||||
const api = load('src/axios/axiosInstance.ts').default;
|
||||
api.defaults.adapter = async config => { requests.push(config); return { data: {}, status: 200, headers: {}, config }; };
|
||||
return { catalog: load('src/lib/catalog.ts'), api, load, requests, queries, state, invalidations, downloads, render: (component, props) => { cursor = 0; return component(props); } };
|
||||
}
|
||||
function all(tree) { if (tree == null || typeof tree === 'boolean') return []; if (Array.isArray(tree)) return tree.flatMap(all); if (typeof tree !== 'object') return [tree]; return [tree, ...all(tree.props?.children)]; }
|
||||
function text(tree) { return all(tree).filter(v => typeof v === 'string' || typeof v === 'number').join(' '); }
|
||||
function find(tree, condition) { const value = all(tree).find(v => v && typeof v === 'object' && condition(v)); assert.ok(value, 'Expected UI element'); return value; }
|
||||
const root = 'src/app/(index)/products/';
|
||||
const category = { id: 1, name: 'C', parent: null, order_cat: null, excel_order: null, image: null };
|
||||
const product = { id: 1, name: 'P', price: '100', category: 1, category_name: 'C', acc_code: 'P1', attr: null, view_only: false, order_prd: null, excel_order: null };
|
||||
|
||||
test('server capability and layout fail closed for missing permission, nonadmin and request failure', () => {
|
||||
const h = harness(), Layout = h.load(root+'layout.tsx').default;
|
||||
for (const info of [null, {}, { role: 'workspace_admin' }, { permissions: { manage_products: 'true' } }, { permissions: { manage_products: false } }]) {
|
||||
assert.equal(h.catalog.canManageProducts(info), false);
|
||||
h.queries.set('cuser-info', { data: { data: info } });
|
||||
assert.doesNotMatch(text(h.render(Layout, { children: 'PRIVATE_EDITOR' })), /PRIVATE_EDITOR/);
|
||||
}
|
||||
h.queries.set('cuser-info', { isError: true, error: new Error('Denied'), refetch() {} });
|
||||
assert.doesNotMatch(text(h.render(Layout, { children: 'PRIVATE_EDITOR' })), /PRIVATE_EDITOR/);
|
||||
h.queries.set('cuser-info', { data: { data: { permissions: { manage_products: true } } } });
|
||||
assert.match(text(h.render(Layout, { children: 'PRIVATE_EDITOR' })), /PRIVATE_EDITOR/);
|
||||
});
|
||||
test('numeric inputs preserve exact bigint price and accept Persian Arabic digits', () => {
|
||||
const h = harness();
|
||||
assert.equal(h.catalog.integerInput('۹٬۲۲۳', 'قیمت'), '9223');
|
||||
assert.equal(h.catalog.integerInput('١٢٣', 'ترتیب'), '123');
|
||||
assert.equal(h.catalog.integerInput('9223372036854775807', 'قیمت', false, '9223372036854775807'), '9223372036854775807');
|
||||
assert.equal(h.catalog.integerInput('', 'ترتیب', true), null);
|
||||
for (const value of ['-1','1.5','NaN','2147483648']) assert.throws(() => h.catalog.integerInput(value, 'ترتیب'));
|
||||
});
|
||||
test('category path renders ancestors and stops invalid cycles', () => {
|
||||
const h = harness(), parent = { ...category, name: 'Parent' }, child = { ...category, id: 2, parent: 1, name: 'Child' };
|
||||
assert.equal(h.catalog.categoryLabel(child, [parent, child]), 'Parent / Child');
|
||||
parent.parent = 2; assert.ok(h.catalog.categoryLabel(child, [parent, child]).length < 100);
|
||||
});
|
||||
test('API client uses protected routes, authenticated requests and exact bulk payload', async () => {
|
||||
const h = harness();
|
||||
await h.catalog.listProducts({ search: 'P', page: 2 });
|
||||
await h.catalog.saveProduct({ name: 'P', price: '1', category: 1 }, 7);
|
||||
await h.catalog.deleteProduct(8); await h.catalog.bulkProducts([7,8], 'move', { category: 2 });
|
||||
assert.equal(h.requests[0].url, 'shop/manage/products/'); assert.equal(h.requests[0].params.search, 'P');
|
||||
assert.equal(h.requests[1].method, 'patch'); assert.equal(h.requests[2].method, 'delete');
|
||||
assert.deepEqual(JSON.parse(h.requests[3].data).ids, [7,8]); assert.equal(h.requests[0].headers.Authorization, 'Bearer token');
|
||||
});
|
||||
test('product form displays duplicate code errors and allows retry', async () => {
|
||||
const h = harness(), Form = h.load(root+'components/product-form.tsx').default;
|
||||
h.api.defaults.adapter = async config => { throw new axios.AxiosError('Duplicate', 'ERR_BAD_REQUEST', config, null, { data: { acc_code: ['کد محصول تکراری است.'] }, status: 400, headers: {}, config }); };
|
||||
const props = { categories: [category], onSaved() { throw new Error('Should not succeed'); }, onCancel() {} };
|
||||
await h.render(Form, props).props.onSubmit({ preventDefault() {}, currentTarget: { name: 'P', price: '10', category: '1', acc_code: 'P1', attr: '', order_prd: '', excel_order: '' } });
|
||||
const tree = h.render(Form, props);
|
||||
assert.match(text(tree), /کد محصول تکراری/); assert.equal(find(tree, v => v.type === 'button' && text(v).includes('ذخیره')).props.disabled, false);
|
||||
});
|
||||
test('product form validates attributes and submits large price as string', async () => {
|
||||
const h = harness(), Form = h.load(root+'components/product-form.tsx').default;
|
||||
let saved = 0; const props = { categories: [category], onSaved: () => saved++, onCancel() {} };
|
||||
const data = { name: 'P', price: '9223372036854775807', category: '1', acc_code: 'P1', attr: '[]', order_prd: '', excel_order: '' };
|
||||
await h.render(Form, props).props.onSubmit({ preventDefault() {}, currentTarget: data });
|
||||
assert.equal(h.requests.length, 0); assert.match(text(h.render(Form, props)), /شیء JSON/);
|
||||
data.attr = '{}'; await h.render(Form, props).props.onSubmit({ preventDefault() {}, currentTarget: data });
|
||||
assert.equal(JSON.parse(h.requests[0].data).price, '9223372036854775807'); assert.equal(saved, 1);
|
||||
});
|
||||
test('products page applies filters and exposes editing controls', () => {
|
||||
const h = harness(), Page = h.load(root+'page.tsx').default;
|
||||
h.queries.set('managed-products', { data: { count: 1, results: [product], next: null } }); h.queries.set('managed-categories', { data: [category] });
|
||||
const tree = h.render(Page);
|
||||
find(tree, v => v.type === 'form').props.onSubmit({ preventDefault() {}, currentTarget: { search: 'P1', ordering: 'price', category: '1', view_only: '' } });
|
||||
assert.equal(h.state[0].search, 'P1'); assert.equal(h.state[1], 1);
|
||||
assert.match(text(tree), /افزودن محصول/); assert.match(text(tree), /ویرایش/); assert.match(text(tree), /حذف/);
|
||||
});
|
||||
test('category form sends parent and independently nullable ordering', async () => {
|
||||
const h = harness(), Page = h.load(root+'categories/page.tsx').default;
|
||||
h.queries.set('managed-categories', { data: [category] });
|
||||
let tree = h.render(Page); find(tree, v => v.type === 'button' && text(v).includes('افزودن دسته')).props.onClick();
|
||||
tree = h.render(Page);
|
||||
await find(tree, v => v.type === 'form').props.onSubmit({ preventDefault() {}, currentTarget: { name: 'Child', parent: '1', order_cat: '', excel_order: '۴', image: null } });
|
||||
const body = JSON.parse(h.requests[0].data); assert.equal(body.parent, 1); assert.equal(body.order_cat, null); assert.equal(body.excel_order, 4);
|
||||
assert.ok(h.invalidations.includes('managed-categories'));
|
||||
});
|
||||
test('Excel page displays per-row errors and blocks atomic confirmation', async () => {
|
||||
const h = harness(), Page = h.load(root+'import/page.tsx').default;
|
||||
const batch = { id: 3, filename: 'file.xlsx', created: 1, updated: 0, errors: 1, committed: false, can_confirm: false, expires_at: '2099-01-01', row_count: 2, row_page: 1, options: { errors: 'atomic' }, rows: [{ row: 3, values: { name: 'Bad', acc_code: 'BAD', category: 'C' }, status: 'error', errors: { price: ['قیمت نامعتبر است.'] } }] };
|
||||
h.api.defaults.adapter = async config => { h.requests.push(config); return { data: batch, status: 201, headers: {}, config }; };
|
||||
let tree = h.render(Page);
|
||||
await find(tree, v => v.type === 'form').props.onSubmit({ preventDefault() {}, currentTarget: { file: 'xlsx', mode: 'upsert', errors: 'atomic', create_categories: 'on', mapping: '', ordering: 'preserve' } });
|
||||
tree = h.render(Page); assert.match(text(tree), /قیمت نامعتبر/);
|
||||
assert.equal(find(tree, v => v.type === 'button' && text(v).includes('تأیید و ثبت نهایی')).props.disabled, true);
|
||||
assert.equal(h.requests[0].url, 'shop/manage/import/preview/'); assert.equal(h.requests[0].timeout, 120000);
|
||||
assert.ok(h.invalidations.includes('product-imports'));
|
||||
});
|
||||
test('download errors retain API message and do not download a false success file', async () => {
|
||||
const h = harness();
|
||||
h.api.defaults.adapter = async config => { throw new axios.AxiosError('Forbidden', 'ERR_BAD_RESPONSE', config, null, { status: 403, data: new Blob([JSON.stringify({ detail: 'دسترسی ندارید.' })]), headers: {}, config }); };
|
||||
await assert.rejects(h.catalog.downloadCatalog('products'), /دسترسی ندارید/); assert.equal(h.downloads.length, 0);
|
||||
});
|
||||
|
||||
test('sidebar only offers product management for explicit admin capability', () => {
|
||||
const h = harness(), Sidebar = h.load('src/app/(index)/components/side-right.tsx').default;
|
||||
h.queries.set('cuser-info', { data: { data: { permissions: { manage_products: false } } } });
|
||||
assert.doesNotMatch(text(h.render(Sidebar)), /مدیریت محصولات/);
|
||||
h.queries.set('cuser-info', { data: { data: { permissions: { manage_products: true } } } });
|
||||
assert.match(text(h.render(Sidebar)), /مدیریت محصولات/);
|
||||
});
|
||||
Reference in new issue
Block a user