From 6d36739683c783b110384c6b56bc068830a8ee9d Mon Sep 17 00:00:00 2001 From: Michael Wedl Date: Thu, 9 Jan 2025 16:33:07 +0100 Subject: [PATCH 1/4] Sort designs by usage count --- api/pyproject.toml | 3 ++- api/src/reportcreator_api/pentests/admin.py | 2 ++ .../migrations/0060_projecttype_usage_count.py | 18 ++++++++++++++++++ .../pentests/models/project.py | 3 ++- .../reportcreator_api/pentests/querysets.py | 7 ++++++- .../pentests/serializers/project.py | 12 +++++++++--- .../pentests/serializers/project_type.py | 4 +++- api/src/reportcreator_api/pentests/views.py | 6 +++++- api/src/reportcreator_api/tests/test_api2.py | 11 +++++++++++ .../reportcreator_api/tests/test_history.py | 4 ++-- .../tests/test_import_export.py | 2 ++ .../src/components/S/ProjectTypeSelection.vue | 2 +- packages/nuxt-base-layer/src/utils/types.ts | 1 + 13 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 api/src/reportcreator_api/pentests/migrations/0060_projecttype_usage_count.py diff --git a/api/pyproject.toml b/api/pyproject.toml index 9d5f55e6..7c8e2d0e 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -82,7 +82,8 @@ extend-select = [ ignore = [ "E741", # Ambiguous variable name: `l` "DJ001", # Avoid using null=True on string-based fields in django models - "PT004", # Fixture setUp does not return anything, add leading underscore "S101", # Use of `assert` detected ] +[tool.ruff.lint.flake8-pytest-style] +mark-parentheses = true \ No newline at end of file diff --git a/api/src/reportcreator_api/pentests/admin.py b/api/src/reportcreator_api/pentests/admin.py index 08c20612..e4c9b6d9 100644 --- a/api/src/reportcreator_api/pentests/admin.py +++ b/api/src/reportcreator_api/pentests/admin.py @@ -33,6 +33,7 @@ @admin.register(ProjectType) class ProjectTypeAdmin(SimpleHistoryAdmin, BaseAdmin): list_display = ['id', 'name', 'created', 'status', 'tags'] + readonly_fields = ('default_notes',) def link_projects(self, obj): return admin_changelist_url('Projects using this ProjectType', 'pentests', 'pentestproject', {'project_type_id': obj.id}) @@ -54,6 +55,7 @@ class ProjectMemberInfoInlineAdmin(admin.StackedInline): class PentestProjectAdmin(SimpleHistoryAdmin, BaseAdmin): list_display = ['id', 'name', 'created', 'readonly', 'tags'] inlines = [ProjectMemberInfoInlineAdmin] + readonly_fields = ('imported_members',) def link_findings(self, obj): return admin_changelist_url('Findings of this project', 'pentests', 'pentestfinding', {'project_id': obj.id}) diff --git a/api/src/reportcreator_api/pentests/migrations/0060_projecttype_usage_count.py b/api/src/reportcreator_api/pentests/migrations/0060_projecttype_usage_count.py new file mode 100644 index 00000000..294df85d --- /dev/null +++ b/api/src/reportcreator_api/pentests/migrations/0060_projecttype_usage_count.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-01-09 14:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pentests', '0059_alter_findingtemplatetranslation_language_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='projecttype', + name='usage_count', + field=models.PositiveIntegerField(db_index=True, default=0), + ), + ] diff --git a/api/src/reportcreator_api/pentests/models/project.py b/api/src/reportcreator_api/pentests/models/project.py index 8ab7b5a3..afe0df19 100644 --- a/api/src/reportcreator_api/pentests/models/project.py +++ b/api/src/reportcreator_api/pentests/models/project.py @@ -78,6 +78,7 @@ class ProjectType(LockableMixin, LanguageMixin, ImportableMixin, BaseModel): max_length=20, choices=ProjectTypeStatus.choices, default=ProjectTypeStatus.IN_PROGRESS, db_index=True, ) tags = ArrayField(base_field=models.CharField(max_length=255), default=list, blank=True, db_index=True) + usage_count = models.PositiveIntegerField(default=0, db_index=True) # PDF Template report_template = EncryptedField(base_field=models.TextField(default="", blank=True)) @@ -117,7 +118,7 @@ class ProjectType(LockableMixin, LanguageMixin, ImportableMixin, BaseModel): copy_of = models.ForeignKey(to="ProjectType", on_delete=models.SET_NULL, null=True, blank=True) - history = HistoricalRecords(cascade_delete_history=True) + history = HistoricalRecords(cascade_delete_history=True, excluded_fields=['usage_count']) objects = querysets.ProjectTypeManager() class Meta: diff --git a/api/src/reportcreator_api/pentests/querysets.py b/api/src/reportcreator_api/pentests/querysets.py index 9fc17683..fdf90872 100644 --- a/api/src/reportcreator_api/pentests/querysets.py +++ b/api/src/reportcreator_api/pentests/querysets.py @@ -93,6 +93,9 @@ def custom_finding_field_definitions(self): all_fields[f['id']] = f return parse_field_definition(all_fields.values()) + def increment_usage_count(self, by=1): + return self.update(usage_count=models.F('usage_count') + models.Value(by)) + class ProjectTypeManager(models.Manager.from_queryset(ProjectTypeQueryset)): use_in_migrations = True @@ -107,6 +110,7 @@ def copy(self, instance, **kwargs): # Copy model instance = copy.copy(instance) instance.created = timezone.now() + instance.usage_count = 0 for k, v in (kwargs or {}).items(): setattr(instance, k, v) instance.copy_of_id = instance.pk @@ -173,6 +177,7 @@ def copy(self, instance, **kwargs): instance.project_type = instance.project_type.copy( linked_user=None, source=SourceEnum.SNAPSHOT if instance.project_type.source not in [SourceEnum.IMPORTED_DEPENDENCY, SourceEnum.CUSTOMIZED] else instance.project_type.source, + usage_count=1, ) instance.skip_post_create_signal = True instance.save() @@ -386,13 +391,13 @@ def copy(self, instance, **kwargs): # Copy model instance = copy.copy(instance) instance.created = timezone.now() + instance.usage_count = 0 for k, v in (kwargs or {}).items(): setattr(instance, k, v) instance.copy_of_id = instance.pk instance.pk = None instance.main_translation = None instance.lock_info_data = None - instance.usage_count = 0 instance.skip_post_create_signal = True instance.save_without_historical_record() diff --git a/api/src/reportcreator_api/pentests/serializers/project.py b/api/src/reportcreator_api/pentests/serializers/project.py index feedeffa..25590db9 100644 --- a/api/src/reportcreator_api/pentests/serializers/project.py +++ b/api/src/reportcreator_api/pentests/serializers/project.py @@ -25,6 +25,7 @@ PentestProject, ProjectMemberInfo, ProjectMemberRole, + ProjectType, ReportSection, SourceEnum, UploadedImage, @@ -376,7 +377,8 @@ def validate_project_type(self, value): @transaction.atomic @history_context() def create(self, validated_data): - project_type = validated_data.pop('project_type').copy(linked_user=None, source=SourceEnum.SNAPSHOT) + project_type_original = validated_data.pop('project_type') + project_type = project_type_original.copy(linked_user=None, source=SourceEnum.SNAPSHOT, usage_count=1) validated_data.pop('force_change_project_type') members = validated_data.pop('members', []) @@ -400,6 +402,7 @@ def create(self, validated_data): project_type.linked_project = project project_type.save(update_fields=['linked_project']) + ProjectType.objects.filter(id=project_type_original.id).increment_usage_count() sysreptor_signals.post_create.send(sender=project.__class__, instance=project) return project @@ -409,10 +412,12 @@ def update(self, instance, validated_data): if (imported_members := validated_data.get('imported_members')) is not None: validated_data['imported_members'] = self.fields['imported_members'].update(instance.imported_members, imported_members) if (project_type := validated_data.get('project_type')) and instance.project_type != project_type and project_type.linked_project != instance: + ProjectType.objects.filter(id=instance.project_type.id).increment_usage_count() validated_data['project_type'] = project_type.copy( linked_project=instance, linked_user=None, - source=SourceEnum.SNAPSHOT) + source=SourceEnum.SNAPSHOT, + usage_count=1) instance = super().update(instance, validated_data) if members is not None: @@ -510,7 +515,8 @@ def update(self, instance, validated_data): name='Customization of ' + instance.project_type.name, source=SourceEnum.CUSTOMIZED, linked_project=instance, - linked_user=None) + linked_user=None, + usage_count=1) instance.save() return instance diff --git a/api/src/reportcreator_api/pentests/serializers/project_type.py b/api/src/reportcreator_api/pentests/serializers/project_type.py index 87ec83bf..996f31ca 100644 --- a/api/src/reportcreator_api/pentests/serializers/project_type.py +++ b/api/src/reportcreator_api/pentests/serializers/project_type.py @@ -13,9 +13,10 @@ class Meta: model = ProjectType fields = [ 'id', 'created', 'updated', 'source', 'scope', - 'name', 'language', 'status', 'tags', + 'name', 'language', 'status', 'tags', 'usage_count', 'details', 'assets', ] + read_only_fields = ['usage_count'] class ProjectTypeDetailSerializer(ProjectTypeShortSerializer): @@ -102,6 +103,7 @@ def update(self, instance, validated_data): return instance.copy( name='Copy of ' + instance.name, source=SourceEnum.CREATED, + usage_count=instance.usage_count, **({ ProjectTypeScope.GLOBAL: {'linked_user': None, 'linked_project': None}, ProjectTypeScope.PRIVATE: {'linked_user': self.context['request'].user, 'linked_project': None}, diff --git a/api/src/reportcreator_api/pentests/views.py b/api/src/reportcreator_api/pentests/views.py index 6ffedab1..b45aeb21 100644 --- a/api/src/reportcreator_api/pentests/views.py +++ b/api/src/reportcreator_api/pentests/views.py @@ -509,7 +509,7 @@ def filter_linked_project(self, queryset, name, value): class ProjectTypeOrderingFilter(OrderingFilter): - ordering_fields = ['created', 'updated', 'name', 'scope', 'status'] + ordering_fields = ['created', 'updated', 'name', 'scope', 'status', 'usage'] def get_queryset_ordering(self, request, queryset, view): out = [] @@ -522,6 +522,10 @@ def get_queryset_ordering(self, request, queryset, view): out.append('status_order') elif o == '-status': out.append('-status_order') + elif o == 'usage': + out.append('usage_count') + elif o == '-usage': + out.append('-usage_count') else: out.append(o) return out diff --git a/api/src/reportcreator_api/tests/test_api2.py b/api/src/reportcreator_api/tests/test_api2.py index 4c807930..88c4e497 100644 --- a/api/src/reportcreator_api/tests/test_api2.py +++ b/api/src/reportcreator_api/tests/test_api2.py @@ -49,6 +49,9 @@ def test_create_project(self): # ProjectType copied on create assert p['project_type'] != str(self.project_type.id) assert self.client.get(reverse('projecttype-detail', kwargs={'pk': p['project_type']})).json()['source'] == SourceEnum.SNAPSHOT + # ProjectType.usage_count incremented + self.project_type.refresh_from_db() + assert self.project_type.usage_count == 1 def test_copy_project(self): project = create_project(project_type=self.project_type, members=[self.user]) @@ -81,6 +84,10 @@ def test_change_design(self): assert pt.source == SourceEnum.SNAPSHOT assert pt.linked_project == project + # ProjectType.usage_count incremented + self.project_type.refresh_from_db() + assert self.project_type.usage_count == 1 + def test_change_imported_members(self): project = create_project(members=[self.user], imported_members=[{ 'id': uuid4(), @@ -318,6 +325,10 @@ def test_create_finding_from_template(self): assert f2.data['data']['title'] == 'title translation' assert f2.data['data']['description'] == 'description main' + # Template.usage_count incremented + self.template.refresh_from_db() + assert self.template.usage_count == 2 + def test_create_finding_from_template_images(self): template = create_template(data={'description': '![image](/images/name/image.png)'}, images_kwargs=[{'name': 'image.png'}]) project = create_project(members=[self.user], findings_kwargs=[], images_kwargs=[]) diff --git a/api/src/reportcreator_api/tests/test_history.py b/api/src/reportcreator_api/tests/test_history.py index 4583b97c..026d1fb8 100644 --- a/api/src/reportcreator_api/tests/test_history.py +++ b/api/src/reportcreator_api/tests/test_history.py @@ -393,7 +393,7 @@ def add_history_test(): for h in history: res_pt = self.client.get(reverse('projecttypehistory-detail', kwargs={'projecttype_pk': pt.id, 'history_date': h['history_date'].isoformat()})) assert res_pt.status_code == 200 - assert omit_keys(res_pt.data, ['updated', 'lock_info']) == omit_keys(h['instance'], ['updated', 'lock_info']) + assert omit_keys(res_pt.data, ['updated', 'lock_info', 'usage_count']) == omit_keys(h['instance'], ['updated', 'lock_info', 'usage_count']) for name, content in h['assets'].items(): res_i = self.client.get(reverse('projecttypehistory-asset-by-name', kwargs={'projecttype_pk': pt.id, 'history_date': h['history_date'].isoformat(), 'filename': name})) assert res_i.status_code == 200 @@ -577,7 +577,7 @@ def add_history_test(): assert {f['id']: omit_keys(f, ['updated']) for f in res_p.data['findings']} == {f['id']: omit_keys(f, ['updated']) for f in h['project']['findings']} res_pt = self.client.get(reverse('projecttypehistory-detail', kwargs={'projecttype_pk': h['project_type']['id'], 'history_date': h['history_date']})) - assert omit_keys(res_pt.data, ['updated', 'lock_info']) == omit_keys(h['project_type'], ['updated', 'lock_info']) + assert omit_keys(res_pt.data, ['updated', 'lock_info', 'usage_count']) == omit_keys(h['project_type'], ['updated', 'lock_info', 'usage_count']) for fh in h['findings']: res_f = self.client.get(reverse('pentestprojecthistory-finding', kwargs=url_kwargs | {'id': fh['id']})) diff --git a/api/src/reportcreator_api/tests/test_import_export.py b/api/src/reportcreator_api/tests/test_import_export.py index 1a144865..7f45dd4d 100644 --- a/api/src/reportcreator_api/tests/test_import_export.py +++ b/api/src/reportcreator_api/tests/test_import_export.py @@ -558,6 +558,7 @@ def test_copy_project(self): self.assert_project_type_copy_equal(p.project_type, cp.project_type, exclude_fields=['source', 'linked_project']) assert cp.project_type.source == SourceEnum.SNAPSHOT assert cp.project_type.linked_project == cp + assert cp.project_type.usage_count == 1 assert members_equal(p.members, cp.members) assert set(p.images.values_list('id', flat=True)).intersection(cp.images.values_list('id', flat=True)) == set() @@ -593,6 +594,7 @@ def test_copy_project_type(self): cp = pt.copy() self.assert_project_type_copy_equal(pt, cp) + assert cp.usage_count == 0 def test_copy_template(self): user = create_user() diff --git a/packages/nuxt-base-layer/src/components/S/ProjectTypeSelection.vue b/packages/nuxt-base-layer/src/components/S/ProjectTypeSelection.vue index 2172486b..173c28fc 100644 --- a/packages/nuxt-base-layer/src/components/S/ProjectTypeSelection.vue +++ b/packages/nuxt-base-layer/src/components/S/ProjectTypeSelection.vue @@ -66,7 +66,7 @@ const emit = defineEmits<{ const items = useSearchableCursorPaginationFetcher({ baseURL: '/api/v1/projecttypes/', query: { - ordering: 'status,scope,name,-created', + ordering: 'status,scope,-usage,name,-created', scope: [ProjectTypeScope.GLOBAL, ProjectTypeScope.PRIVATE], ...props.queryFilters } diff --git a/packages/nuxt-base-layer/src/utils/types.ts b/packages/nuxt-base-layer/src/utils/types.ts index 371fc5be..ba44d03c 100644 --- a/packages/nuxt-base-layer/src/utils/types.ts +++ b/packages/nuxt-base-layer/src/utils/types.ts @@ -486,6 +486,7 @@ export type ReportSectionDefinition = { export type ProjectType = BaseModel & Lockable & { readonly source: SourceEnum; readonly scope: ProjectTypeScope; + readonly usage_count: number; readonly copy_of: string|null; name: string; From a681edde87e5ca6035046fb11f7839a67d22d467 Mon Sep 17 00:00:00 2001 From: Michael Wedl Date: Thu, 9 Jan 2025 16:57:41 +0100 Subject: [PATCH 2/4] Pre-select first design when creating projects --- .../src/components/Template/FieldSelector.vue | 1 + .../src/pages/projects/[projectId]/index.vue | 3 +++ packages/frontend/src/pages/projects/new.vue | 1 + .../src/components/S/ProjectTypeSelection.vue | 13 +++++++++++++ 4 files changed, 18 insertions(+) diff --git a/packages/frontend/src/components/Template/FieldSelector.vue b/packages/frontend/src/components/Template/FieldSelector.vue index 967e1c12..c805ba36 100644 --- a/packages/frontend/src/components/Template/FieldSelector.vue +++ b/packages/frontend/src/components/Template/FieldSelector.vue @@ -6,6 +6,7 @@ :query-filters="{scope: [ProjectTypeScope.GLOBAL]}" :additional-items="([{id: 'all', name: 'All Designs'}] as ProjectType[])" :disabled="disabled" + :required="true" label="Show fields of" variant="underlined" /> diff --git a/packages/frontend/src/pages/projects/[projectId]/index.vue b/packages/frontend/src/pages/projects/[projectId]/index.vue index 78f8d01e..95e04526 100644 --- a/packages/frontend/src/pages/projects/[projectId]/index.vue +++ b/packages/frontend/src/pages/projects/[projectId]/index.vue @@ -73,6 +73,7 @@ :error-messages="serverErrors?.project_type || []" :readonly="readonly" :append-link="true" + :required="true" return-object class="mt-4" > @@ -146,6 +147,7 @@ import { ProjectTypeScope } from '#imports'; const route = useRoute(); const auth = useAuth(); const projectStore = useProjectStore(); +const projectTypeStore = useProjectTypeStore(); const project = await useFetchE(`/api/v1/pentestprojects/${route.params.projectId}/`, { method: 'GET', key: 'projectSettings:project', deep: true }); const serverErrors = ref(null); @@ -179,6 +181,7 @@ const { toolbarAttrs, readonly, editMode } = useLockEdit({ project.value = await projectStore.partialUpdateProject(project.value, ['name', 'project_type', 'force_change_project_type', 'language', 'tags', 'members', 'imported_members']); serverErrors.value = null; + projectType.value = await projectTypeStore.getById(project.value.project_type); } catch (error: any) { if (error?.status === 400 && error?.data) { serverErrors.value = error.data; diff --git a/packages/frontend/src/pages/projects/new.vue b/packages/frontend/src/pages/projects/new.vue index 385ba2aa..5c60ac80 100644 --- a/packages/frontend/src/pages/projects/new.vue +++ b/packages/frontend/src/pages/projects/new.vue @@ -23,6 +23,7 @@ diff --git a/packages/nuxt-base-layer/src/components/S/ProjectTypeSelection.vue b/packages/nuxt-base-layer/src/components/S/ProjectTypeSelection.vue index 173c28fc..86f19356 100644 --- a/packages/nuxt-base-layer/src/components/S/ProjectTypeSelection.vue +++ b/packages/nuxt-base-layer/src/components/S/ProjectTypeSelection.vue @@ -101,6 +101,19 @@ useLazyAsyncData(async () => { emit('update:modelValue', initialProjectType.value); } } + + if (props.required && !props.modelValue) { + if (props.additionalItems.length === 0) { + await items.fetchNextPage(); + } + if (allItems.value.length > 0) { + if (props.returnObject) { + emit('update:modelValue', allItems.value[0]!); + } else { + emit('update:modelValue', allItems.value[0]!.id); + } + } + } }); const allItems = computed(() => { From 054551f3a4b881c24dcf335bdd0c1fd4b014ebe9 Mon Sep 17 00:00:00 2001 From: Michael Wedl Date: Mon, 13 Jan 2025 06:52:52 +0100 Subject: [PATCH 3/4] Fix usage_count increment on change design --- api/src/reportcreator_api/pentests/serializers/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/reportcreator_api/pentests/serializers/project.py b/api/src/reportcreator_api/pentests/serializers/project.py index 25590db9..ce120cd1 100644 --- a/api/src/reportcreator_api/pentests/serializers/project.py +++ b/api/src/reportcreator_api/pentests/serializers/project.py @@ -412,7 +412,7 @@ def update(self, instance, validated_data): if (imported_members := validated_data.get('imported_members')) is not None: validated_data['imported_members'] = self.fields['imported_members'].update(instance.imported_members, imported_members) if (project_type := validated_data.get('project_type')) and instance.project_type != project_type and project_type.linked_project != instance: - ProjectType.objects.filter(id=instance.project_type.id).increment_usage_count() + ProjectType.objects.filter(id=project_type.id).increment_usage_count() validated_data['project_type'] = project_type.copy( linked_project=instance, linked_user=None, From 632dae44159c568b7339049b4e16eaf12d5a98e7 Mon Sep 17 00:00:00 2001 From: Michael Wedl Date: Mon, 13 Jan 2025 07:44:11 +0100 Subject: [PATCH 4/4] Update integration test for design selector --- .../test/e2e/integration/designs.test.spec.ts | 13 ++------- .../test/e2e/integration/projects.spec.ts | 28 ++++++------------- packages/frontend/test/e2e/util/helpers.ts | 22 +++++++++++++++ 3 files changed, 33 insertions(+), 30 deletions(-) create mode 100644 packages/frontend/test/e2e/util/helpers.ts diff --git a/packages/frontend/test/e2e/integration/designs.test.spec.ts b/packages/frontend/test/e2e/integration/designs.test.spec.ts index 77ef2144..bd0a2bc4 100644 --- a/packages/frontend/test/e2e/integration/designs.test.spec.ts +++ b/packages/frontend/test/e2e/integration/designs.test.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; import { DemoDataState, DemoDataType } from '../util/demo_data'; +import { createProject } from '../util/helpers'; const designName = 'My Test Design'; test('A User can create an Design with a Name', async ({ page }) => { @@ -45,16 +46,8 @@ test('A User can create an Design with a Name', async ({ page }) => { test('Design Settings are reflected in a Project', async ({ page }) => { const testState = new DemoDataState(); - await page.goto('/projects'); - await page.waitForSelector('text=Projects'); - await page.getByTestId('create-button').click(); - await page.getByLabel('Name').fill('My Design Test Project'); - await page.getByTestId('project-type').getByRole('textbox').fill(designName); - await page.getByTestId('page-loader').waitFor({ state: 'hidden' }); - await page.getByText('No data found').waitFor({ state: 'hidden' }); - const designId = testState.designs.at(testState.designs.length - 1); - await page.getByTestId(`design-${designId}`).click(); - await page.getByTestId('submit-project').click(); + await createProject(page, { projectName: 'My Design Test Project', designId: testState.designs.at(testState.designs.length - 1)!, designName: designName }); + await page.getByRole('link', { name: 'Executive Summary' }).click(); await page.getByLabel('Assignee', { exact: true }).waitFor(); expect(await page.getByRole('textbox').getByText('This is a test executive summary').isVisible()).toBeTruthy(); diff --git a/packages/frontend/test/e2e/integration/projects.spec.ts b/packages/frontend/test/e2e/integration/projects.spec.ts index 9c591069..b03a4864 100644 --- a/packages/frontend/test/e2e/integration/projects.spec.ts +++ b/packages/frontend/test/e2e/integration/projects.spec.ts @@ -1,29 +1,17 @@ import { expect, test } from '@playwright/test'; import { DemoDataState } from '../util/demo_data'; -const projectName = 'My Test Project'; +import { createProject } from '../util/helpers'; +const projectName = 'Updated Project Name'; test('A User can create an Project with a Name', async ({ page }) => { - await page.goto('/projects'); - await page.waitForSelector('text=Projects'); - - expect(await page.title()).toBe('Projects | SysReptor'); - // Create New Design Modal - await page.getByTestId('create-button').click(); - // Create New Design Modal - await page.getByLabel('Name').fill(projectName); - const designId = new DemoDataState().designs[0]; - - await page.getByTestId('project-type').getByRole('textbox').fill('Demo Matrix'); - await page.getByTestId('page-loader').waitFor({ state: 'hidden' }); - await page.getByText('No data found').waitFor({ state: 'hidden' }); - await page.getByTestId('design-' + designId).locator('nth=0').click(); - await page.getByTestId('submit-project').click(); + const oldProjectName = 'My Test Project' + await createProject(page, { projectName: oldProjectName, designId: new DemoDataState().designs[0]!, designName: 'Demo Matrix' }); // Verify Project Name - await expect(page.getByText(projectName)).toBeVisible(); + await expect(page.getByText(oldProjectName)).toBeVisible(); await page.getByTestId('project-settings-tab').click(); await page.getByLabel('Name').waitFor(); - await page.getByLabel('Name').fill('Updated Project Name'); + await page.getByLabel('Name').fill(projectName); await page.getByLabel('Tags').fill('Updated Tags'); await page.keyboard.press('Enter'); await page.getByRole('button', { name: 'Badge' }).click(); @@ -33,11 +21,11 @@ test('A User can create an Project with a Name', async ({ page }) => { test('A User can delete a Project', async ({ page }) => { await page.goto('/projects'); await page.waitForSelector('text=Projects'); - await page.getByRole('link', { name: 'Updated Project Name' }).click(); + await page.getByRole('link', { name: projectName }).click(); await page.getByTestId('project-settings-tab').click(); await page.getByTestId('options-dots').click(); await page.getByText('Delete').click(); - await page.getByTestId('confirm-input').getByRole('textbox').fill('Updated Project Name'); + await page.getByTestId('confirm-input').getByRole('textbox').fill(projectName); await page.getByRole('button', { name: 'Delete' }).click(); await page.waitForSelector('text=Projects'); diff --git a/packages/frontend/test/e2e/util/helpers.ts b/packages/frontend/test/e2e/util/helpers.ts new file mode 100644 index 00000000..20437564 --- /dev/null +++ b/packages/frontend/test/e2e/util/helpers.ts @@ -0,0 +1,22 @@ +import type { Page } from "@playwright/test"; + +export async function createProject(page: Page, options: { projectName: string, designId: string, designName: string }) { + // Click create project button + await page.goto('/projects'); + await page.waitForSelector('text=Projects'); + await page.getByTestId('create-button').click(); + + // Fill project name + await page.getByLabel('Name').fill(options.projectName); + + // Select design + const textbox = await page.getByTestId('project-type').getByRole('textbox'); + textbox.clear(); + textbox.fill(options.designName); + await page.getByTestId('page-loader').waitFor({ state: 'hidden' }); + await page.getByText('No data found').waitFor({ state: 'hidden' }); + await page.getByTestId(`design-${options.designId}`).click(); + + // Create + await page.getByTestId('submit-project').click(); +}