From 59dac9be5657777f1c7cde555e09286543707e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sat, 16 May 2026 20:05:15 +0300 Subject: [PATCH] =?UTF-8?q?feat(import):=20ImportView=20=E2=80=94=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82?= =?UTF-8?q?=D0=B0=20CSV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TDD: spec (3 tests) first, then component. ImportView.vue: upload form + polling + history table + unknown-statuses banner. Uses api/imports (uploadImport/listImports/getImport/getUnknownStatuses). setInterval callback wrapped in named async fn (pollOnce) — no eslint-disable needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/resources/js/views/ImportView.vue | 237 ++++++++++++++++++++++++++ app/tests/Frontend/ImportView.spec.ts | 74 ++++++++ 2 files changed, 311 insertions(+) create mode 100644 app/resources/js/views/ImportView.vue create mode 100644 app/tests/Frontend/ImportView.spec.ts diff --git a/app/resources/js/views/ImportView.vue b/app/resources/js/views/ImportView.vue new file mode 100644 index 00000000..61b37aef --- /dev/null +++ b/app/resources/js/views/ImportView.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/app/tests/Frontend/ImportView.spec.ts b/app/tests/Frontend/ImportView.spec.ts new file mode 100644 index 00000000..7df4ed75 --- /dev/null +++ b/app/tests/Frontend/ImportView.spec.ts @@ -0,0 +1,74 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount, flushPromises } from '@vue/test-utils'; +import { createVuetify } from 'vuetify'; +import * as components from 'vuetify/components'; +import * as directives from 'vuetify/directives'; + +vi.mock('../../resources/js/api/imports', async (importOriginal) => { + const orig = await importOriginal(); + return { ...orig }; +}); + +const importsApi = await import('../../resources/js/api/imports'); +const ImportView = (await import('../../resources/js/views/ImportView.vue')).default; + +const vuetify = createVuetify({ components, directives }); + +function mountView() { + return mount(ImportView, { + global: { + plugins: [vuetify], + stubs: { UnknownStatusesDialog: true }, + }, + }); +} + +describe('ImportView', () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.spyOn(importsApi, 'listImports').mockResolvedValue([]); + vi.spyOn(importsApi, 'getUnknownStatuses').mockResolvedValue([]); + }); + + it('грузит историю импортов при монтировании', async () => { + const spy = vi.spyOn(importsApi, 'listImports').mockResolvedValue([ + { + id: 1, + filename: 'leads.csv', + status: 'done', + rows_total: 5, + rows_added: 5, + rows_updated: 0, + rows_skipped: 0, + unknown_statuses_count: 0, + dry_run: false, + error_message: null, + started_at: null, + finished_at: null, + }, + ]); + const wrapper = mountView(); + await flushPromises(); + + expect(spy).toHaveBeenCalled(); + expect(wrapper.text()).toContain('leads.csv'); + }); + + it('кнопка загрузки заблокирована без выбранного файла', async () => { + const wrapper = mountView(); + await flushPromises(); + + const uploadBtn = wrapper.find('[data-test="upload-btn"]'); + expect(uploadBtn.attributes('disabled')).toBeDefined(); + }); + + it('показывает баннер о неизвестных статусах', async () => { + vi.spyOn(importsApi, 'getUnknownStatuses').mockResolvedValue([ + { id: 1, status_ru: 'Архив', occurrences: 3 }, + ]); + const wrapper = mountView(); + await flushPromises(); + + expect(wrapper.find('[data-test="unknown-banner"]').exists()).toBe(true); + }); +});