Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion internal/testing/frontend/fe_unit_stubs.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,13 @@ func feUnitPathStubPath(p, joined, importer string, stubs feUnitStubPaths) (stri
return stubs.OPage, true
}
if strings.HasSuffix(p, ".vue") || strings.Contains(joined, ".vue") {
if strings.HasSuffix(p, "ChoyFormView.vue") || strings.HasSuffix(joined, "ChoyFormView.vue") {
return "", false
}
return stubs.ChildView, true
}
case strings.HasSuffix(p, "ChoyFormView.vue") || strings.HasSuffix(joined, "ChoyFormView.vue"):
return "", false
case strings.HasSuffix(p, "FormView.vue") || strings.HasSuffix(joined, "FormView.vue") ||
strings.HasSuffix(p, "ListView.vue") || strings.HasSuffix(joined, "ListView.vue") ||
strings.HasSuffix(p, "KanbanView.vue") || strings.HasSuffix(joined, "KanbanView.vue"):
Expand Down Expand Up @@ -126,7 +131,9 @@ func feUnitPathStubPath(p, joined, importer string, stubs feUnitStubPaths) (stri
return stubs.PageComposable, true
}
case strings.Contains(joined, "/auth/web/stores/auth") || (strings.Contains(p, "stores/auth") && !strings.Contains(importer, "/stores/auth/")):
fromProduct := stubProductChildren || strings.Contains(importer, "Login.vue")
fromProduct := stubProductChildren ||
strings.Contains(importer, "Login.vue") ||
strings.Contains(importer, "Register.vue")
barrel := strings.HasSuffix(p, "/stores/auth") ||
p == "../stores/auth" ||
p == "./stores/auth" ||
Expand Down
5 changes: 5 additions & 0 deletions internal/testing/frontend/host_bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ func BuildFrontendVueHostBundle(opts VueHostBundleOptions) (*BundleResult, error
// or when web/web/components code is the importer (real child mounts).
build.OnResolve(api.OnResolveOptions{Filter: `(FormView|ListView|KanbanView)\.vue$`},
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
path := filepath.ToSlash(args.Path)
// Keep the kit FormView SFC so native submit and field rules run.
if strings.HasSuffix(path, "ChoyFormView.vue") {
return api.OnResolveResult{}, nil
}
importer := filepath.ToSlash(args.Importer)
if strings.Contains(importer, "/web/web/components/") ||
strings.HasSuffix(importer, ".test.ts") ||
Expand Down
9 changes: 9 additions & 0 deletions internal/testing/frontend/host_bundle_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ func TestFeUnitPackageAndPathStubMatchers(t *testing.T) {
{"./Page.vue", "/repo/modules/web/web/components/layout/Page.vue", "/repo/modules/web/web/components/layout/ChoyPageIoMenu.test.ts", "", false},
{"./OPage.vue", "/repo/modules/web/web/components/layout/OPage.vue", "/repo/modules/web/web/components/layout/ChoyPage.storeContext.mount.test.ts", "", false},
{"./PartnerFormView.vue", "/x/PartnerFormView.vue", page, "child", true},
{"@/web/web/components/view/ChoyFormView.vue", "/modules/web/web/components/view/ChoyFormView.vue", page, "", false},
{"@/web/web/components/view/ChoyFormView.vue", "/modules/web/web/components/view/ChoyFormView.vue", "", "", false},
{"./ChoyFormView.vue", "/tmp/ChoyFormView.vue", page, "", false},
{"./ChoyFormView.vue", "/tmp/ChoyFormView.vue", "/other.ts", "", false},
{"./PartnerListView.vue", "/x/PartnerListView.vue", page, "child", true},
{"./ModuleKanbanView.vue", "/x/ModuleKanbanView.vue", view, "child", true},
{"./PartnerFormView.vue", "/x/PartnerFormView.vue", "/other.ts", "", false},
Expand All @@ -101,6 +105,7 @@ func TestFeUnitPackageAndPathStubMatchers(t *testing.T) {
{"@/auth/web/stores/auth", "/modules/auth/web/stores/auth", page, "auth", true},
{"../stores/auth", "/modules/auth/web/stores/auth", page, "auth", true},
{"./stores/auth", "/modules/auth/web/stores/auth", "Login.vue", "auth", true},
{"./stores/auth", "/modules/auth/web/stores/auth", "Register.vue", "auth", true},
{"@/auth/web/stores/auth/index.ts", "/modules/auth/web/stores/auth/index.ts", page, "auth", true},
{"@/web/web/i18n", "/web/web/i18n/index", page, "i18n", true},
{"@/web/web/i18n", "/web/web/i18n/index", "/other.ts", "", false},
Expand Down Expand Up @@ -256,6 +261,10 @@ func TestBuildFrontendVueHostBundle_FEStubsAndExtras(t *testing.T) {
if err != nil || childSkip.Path != "" {
t.Fatalf("child view skip for web unit test: %#v err=%v", childSkip, err)
}
choyFormKeep, err := childViewCB(api.OnResolveArgs{Path: "./ChoyFormView.vue", Importer: "/modules/web/web/kit.ts"})
if err != nil || choyFormKeep.Path != "" {
t.Fatalf("child view skip for kit ChoyFormView: %#v err=%v", choyFormKeep, err)
}
var pathHit, pageFromPage, pageFromTest bool
for _, pathCB := range pathCBs {
pathRes, err := pathCB(api.OnResolveArgs{
Expand Down
3 changes: 3 additions & 0 deletions internal/testing/frontend/testdata/stubs/auth_store.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export const useAuthStore = defineStore('auth-fe-stub', function () {
isAuthenticated.value = true;
return Promise.resolve();
},
register: function () {
return Promise.resolve({ UserId: 'usr_fe_stub' });
},
loadUser: function () {
return Promise.resolve(currentUser.value);
},
Expand Down
70 changes: 52 additions & 18 deletions modules/auth/web/pages/Login.mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,70 @@ async function mountLogin(opts?: {
return { wrapper, replaces };
}

function fieldInput(wrapper: { find: (sel: string) => any }, selector: string) {
const root = wrapper.find(selector);
if (!root.exists()) return root;
const el = root.element as HTMLElement;
if (String(el.tagName || '').toLowerCase() === 'input') return root;
const inner = typeof el.querySelector === 'function' ? el.querySelector('input') : null;
if (inner) {
return {
exists: () => true,
element: inner,
trigger: async (type: string) => {
inner.dispatchEvent(new Event(type, { bubbles: true }));
},
};
}
return root;
}

async function fillField(wrapper: { find: (sel: string) => any }, selector: string, value: string) {
const input = fieldInput(wrapper, selector);
const el = input.element as HTMLInputElement;
el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
el.dispatchEvent(new Event('blur', { bubbles: true, cancelable: true }));
await flushPromises();
}

function submitForm(wrapper: { find: (sel: string) => any }) {
const form = wrapper.find('form');
(form.element as HTMLFormElement).dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
}

async function afterSubmit() {
for (let i = 0; i < 8; i++) await flushPromises();
}

test('Login.vue mounts under choysumMount and runs script setup', async () => {
const { wrapper } = await mountLogin();
expect(wrapper.find('.login-username').exists()).toBe(true);
expect(wrapper.find('.login-password').exists()).toBe(true);
expect(fieldInput(wrapper, '.login-username').exists()).toBe(true);
expect(fieldInput(wrapper, '.login-password').exists()).toBe(true);
wrapper.unmount();
});

test('Login.vue: empty submit keeps the form and shows field errors', async () => {
const { wrapper } = await mountLogin();
const { wrapper, replaces } = await mountLogin();
const form = wrapper.find('form');
expect(form.exists()).toBe(true);
await form.trigger('submit');
await flushPromises();
expect(wrapper.text().includes('Enter username') || wrapper.find('.text-destructive').exists()).toBe(true);
submitForm(wrapper);
await afterSubmit();
expect(wrapper.text().includes('Enter username') || wrapper.find('.text-destructive').exists() || wrapper.find('.text-danger').exists()).toBe(true);
expect(replaces).toEqual([]);
wrapper.unmount();
});

test('Login.vue: successful submit redirects via query.redirect', async () => {
test('Login.vue: filled native submit does not show required field errors', async () => {
const { wrapper, replaces } = await mountLogin({ query: { redirect: '/auth/tokens' } });
const user = wrapper.find('.login-username');
const pass = wrapper.find('.login-password');
expect(user.exists()).toBe(true);
expect(pass.exists()).toBe(true);
(user.element as HTMLInputElement).value = 'admin';
await user.trigger('input');
(pass.element as HTMLInputElement).value = 'secret';
await pass.trigger('input');
await wrapper.find('form').trigger('submit');
await flushPromises();
// Auth stub marks authenticated: success must clear errors and navigate.
expect(fieldInput(wrapper, '.login-username').exists()).toBe(true);
expect(fieldInput(wrapper, '.login-password').exists()).toBe(true);
await fillField(wrapper, '.login-username', 'admin');
await fillField(wrapper, '.login-password', 'secret');
submitForm(wrapper);
await afterSubmit();
expect(wrapper.text().includes('Enter username')).toBe(false);
expect(wrapper.find('.login-error').exists()).toBe(false);
expect(wrapper.find('.text-destructive').exists()).toBe(false);
expect(replaces).toEqual(['/auth/tokens']);
Expand Down
Loading
Loading