Browse Source

Merge remote-tracking branch 'origin' into geodata-prototyping-restart

pull/4140/head
flisowna 2 years ago
parent
commit
1da6159b05
  1. 52
      .github/workflows/playwright-test-workflow.yml
  2. 2
      package.json
  3. 2
      packages/nc-gui/components/cell/MultiSelect.vue
  4. 2
      packages/nc-gui/components/cell/SingleSelect.vue
  5. 2
      packages/nc-gui/components/cell/attachment/index.vue
  6. 8
      packages/nc-gui/components/dashboard/TreeView.vue
  7. 2
      packages/nc-gui/components/dashboard/settings/AuditTab.vue
  8. 4
      packages/nc-gui/components/dashboard/settings/Modal.vue
  9. 2
      packages/nc-gui/components/dlg/TableCreate.vue
  10. 2
      packages/nc-gui/components/general/TruncateText.vue
  11. 46
      packages/nc-gui/components/smartsheet/Form.vue
  12. 4
      packages/nc-gui/components/smartsheet/Gallery.vue
  13. 22
      packages/nc-gui/components/smartsheet/Grid.vue
  14. 2
      packages/nc-gui/components/smartsheet/Kanban.vue
  15. 2
      packages/nc-gui/components/smartsheet/Pagination.vue
  16. 2
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  17. 6
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  18. 2
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  19. 2
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  20. 8
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  21. 2
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  22. 4
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  23. 26
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  24. 2
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  25. 2
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  26. 2
      packages/nc-gui/components/tabs/auth/user-management/ShareBase.vue
  27. 13
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  28. 4
      packages/nc-gui/composables/useColumnCreateStore.ts
  29. 8
      packages/nc-gui/layouts/base.vue
  30. 2
      packages/nc-gui/layouts/default.vue
  31. 2
      packages/nc-gui/layouts/shared-view.vue
  32. 2
      packages/nc-gui/middleware/auth.global.ts
  33. 4
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  34. 6
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  35. 6
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue
  36. 43
      packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue
  37. 6
      packages/nc-gui/pages/index/index/index.vue
  38. 12
      packages/nc-gui/pages/index/index/user.vue
  39. 15
      packages/nc-gui/pages/signin.vue
  40. 79
      packages/nc-gui/tests/playwright/README.md
  41. 5
      packages/nocodb/package.json
  42. 32
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  43. 8
      packages/nocodb/src/lib/plugins/backblaze/Backblaze.ts
  44. 8
      packages/nocodb/src/lib/plugins/gcs/Gcs.ts
  45. 8
      packages/nocodb/src/lib/plugins/linode/LinodeObjectStorage.ts
  46. 8
      packages/nocodb/src/lib/plugins/mino/Minio.ts
  47. 8
      packages/nocodb/src/lib/plugins/ovhCloud/OvhCloud.ts
  48. 8
      packages/nocodb/src/lib/plugins/s3/S3.ts
  49. 8
      packages/nocodb/src/lib/plugins/scaleway/ScalewayObjectStorage.ts
  50. 8
      packages/nocodb/src/lib/plugins/spaces/Spaces.ts
  51. 8
      packages/nocodb/src/lib/plugins/upcloud/UpoCloud.ts
  52. 8
      packages/nocodb/src/lib/plugins/vultr/Vultr.ts
  53. 6
      packages/nocodb/src/lib/utils/pluginUtils.ts
  54. 0
      tests/playwright/.env.example
  55. 0
      tests/playwright/.eslintrc.json
  56. 1
      tests/playwright/.gitignore
  57. 0
      tests/playwright/.lintstagedrc.json
  58. 0
      tests/playwright/.prettierignore
  59. 0
      tests/playwright/.prettierrc.js
  60. 100
      tests/playwright/README.md
  61. 0
      tests/playwright/constants/index.ts
  62. 0
      tests/playwright/fixtures/expectedBaseDownloadData.txt
  63. 0
      tests/playwright/fixtures/expectedBaseDownloadDataPg.txt
  64. 0
      tests/playwright/fixtures/expectedData.txt
  65. 0
      tests/playwright/fixtures/expectedDataSqlite.txt
  66. 0
      tests/playwright/fixtures/sampleFiles/1.json
  67. 0
      tests/playwright/fixtures/sampleFiles/2.json
  68. 0
      tests/playwright/fixtures/sampleFiles/3.json
  69. 0
      tests/playwright/fixtures/sampleFiles/4.json
  70. 0
      tests/playwright/fixtures/sampleFiles/5.json
  71. 0
      tests/playwright/fixtures/sampleFiles/6.json
  72. 0
      tests/playwright/fixtures/sampleFiles/simple.xlsx
  73. 0
      tests/playwright/fixtures/template.spec.ts
  74. 0
      tests/playwright/package-lock.json
  75. 1
      tests/playwright/package.json
  76. 4
      tests/playwright/pages/Base.ts
  77. 15
      tests/playwright/pages/Dashboard/ExpandedForm/index.ts
  78. 70
      tests/playwright/pages/Dashboard/Form/index.ts
  79. 2
      tests/playwright/pages/Dashboard/Gallery/index.ts
  80. 10
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts
  81. 0
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts
  82. 14
      tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts
  83. 2
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  84. 38
      tests/playwright/pages/Dashboard/Grid/index.ts
  85. 0
      tests/playwright/pages/Dashboard/Import/Airtable.ts
  86. 0
      tests/playwright/pages/Dashboard/Import/ImportTemplate.ts
  87. 4
      tests/playwright/pages/Dashboard/Kanban/index.ts
  88. 2
      tests/playwright/pages/Dashboard/Settings/Acl.ts
  89. 2
      tests/playwright/pages/Dashboard/Settings/AppStore.ts
  90. 2
      tests/playwright/pages/Dashboard/Settings/Audit.ts
  91. 2
      tests/playwright/pages/Dashboard/Settings/Erd.ts
  92. 2
      tests/playwright/pages/Dashboard/Settings/Metadata.ts
  93. 2
      tests/playwright/pages/Dashboard/Settings/Miscellaneous.ts
  94. 4
      tests/playwright/pages/Dashboard/Settings/Teams.ts
  95. 2
      tests/playwright/pages/Dashboard/Settings/index.ts
  96. 26
      tests/playwright/pages/Dashboard/SurveyForm/index.ts
  97. 6
      tests/playwright/pages/Dashboard/TreeView.ts
  98. 28
      tests/playwright/pages/Dashboard/ViewSidebar/index.ts
  99. 0
      tests/playwright/pages/Dashboard/WebhookForm/index.ts
  100. 4
      tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts
  101. Some files were not shown because too many files have changed in this diff Show More

52
.github/workflows/playwright-test-workflow.yml

@ -14,6 +14,7 @@ on:
jobs:
playwright:
runs-on: ubuntu-20.04
timeout-minutes: 30
steps:
# Reference: https://github.com/pierotofy/set-swap-space/blob/master/action.yml
- name: Set 5gb swap
@ -55,34 +56,36 @@ jobs:
- name: setup mysql
if: ${{ inputs.db == 'mysql' }}
working-directory: ./
run: docker-compose -f ./packages/nc-gui/tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d &
run: docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d &
- name: setup pg
if: ${{ inputs.db == 'pg' }}
working-directory: ./
run: docker-compose -f ./packages/nc-gui/tests/playwright/scripts/docker-compose-playwright-pg.yml up -d &
run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d &
- name: setup pg for quick tests
if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
working-directory: ./
run: docker-compose -f ./packages/nc-gui/tests/playwright/scripts/docker-compose-pg-pw-quick.yml up -d &
run: docker-compose -f ./tests/playwright/scripts/docker-compose-pg-pw-quick.yml up -d &
- name: run frontend
working-directory: ./packages/nc-gui
run: npm run ci:run
- name: Run backend
working-directory: ./packages/nocodb
run: npm run ci:run &
run: |
npm install
npm run watch:run:playwright > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log &
- name: Cache playwright npm modules
uses: actions/cache@v3
id: playwright-cache
with:
path: |
**/playwright/node_modules
key: cache-nc-playwright-${{ hashFiles('**/playwright/package-lock.json') }}
**/tests/playwright/node_modules
key: cache-nc-playwright-${{ hashFiles('**/tests/playwright/package-lock.json') }}
- name: Install dependencies
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: ./packages/nc-gui/tests/playwright
working-directory: ./tests/playwright
run: npm install
- name: Install Playwright Browsers
working-directory: ./packages/nc-gui/tests/playwright
working-directory: ./tests/playwright
run: npx playwright install chromium --with-deps
- name: Wait for backend
run: |
@ -92,7 +95,7 @@ jobs:
done
- name: Run Playwright tests
working-directory: ./packages/nc-gui/tests/playwright
working-directory: ./tests/playwright
run: E2E_DB_TYPE=${{ inputs.db }} npm run ci:test:shard:${{ inputs.shard }}
# Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1)
@ -102,26 +105,43 @@ jobs:
run: |
kill -9 $(lsof -t -i:8080)
npm run watch:run:playwright:pg:cyquick &
cd ../nc-gui/tests/playwright
npm run test:quick
- name: Run quick server and tests (sqlite)
if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }}
working-directory: ./packages/nocodb
run: |
kill -9 $(lsof -t -i:8080)
npm run watch:run:playwright:quick &
cd ../nc-gui/tests/playwright
npm run test:quick
npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log &
- name: Wait for backend & run quick tests
if: ${{ inputs.db == 'sqlite' }}
working-directory: ./tests/playwright
run: |
while ! curl --output /dev/null --silent --head --fail http://localhost:8080; do
printf '.'
sleep 2
done
PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
- uses: actions/upload-artifact@v3
if: ${{ inputs.db == 'sqlite' }}
with:
name: quick-backend-log-${{ inputs.shard }}
path: ./packages/nocodb/quick_${{ inputs.shard }}_test_backend.log
retention-days: 2
- uses: actions/upload-artifact@v3
if: ${{ inputs.db == 'sqlite' }}
with:
name: playwright-report-quick-${{ inputs.shard }}
path: ./tests/playwright/playwright-report-quick/
retention-days: 2
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report-${{ inputs.db }}-${{ inputs.shard }}
path: ./packages/nc-gui/tests/playwright/playwright-report/
path: ./tests/playwright/playwright-report/
retention-days: 2
- uses: actions/upload-artifact@v3
if: always()
with:
name: backend-logs-${{ inputs.db }}-${{ inputs.shard }}
path: ./packages/nocodb/mysql_test_backend.log
path: ./packages/nocodb/${{ inputs.db }}_${{ inputs.shard }}_test_backend.log
retention-days: 2

2
package.json

@ -36,7 +36,7 @@
]
},
"scripts": {
"lint:staged:playwright": "cd packages/nc-gui/tests/playwright; npx lint-staged; cd -",
"lint:staged:playwright": "cd ./tests/playwright; npx lint-staged; cd -",
"build:common": "cd ./packages/nocodb-sdk; npm install; npm run build",
"install:common": "cd ./packages/nocodb; npm install ../nocodb-sdk; cd ../nc-gui; npm install ../nocodb-sdk",
"start:api": "npm run build:common ; cd ./packages/nocodb; npm install ../nocodb-sdk; npm install; NC_DISABLE_CACHE=true NC_DISABLE_TELE=true npm run watch:run:cypress",

2
packages/nc-gui/components/cell/MultiSelect.vue

@ -155,7 +155,7 @@ watch(isOpen, (n, _o) => {
v-for="op of options"
:key="op.id"
:value="op.title"
:data-nc="`select-option-${column.title}-${rowIndex}`"
:data-testid="`select-option-${column.title}-${rowIndex}`"
@click.stop
>
<a-tag class="rounded-tag" :color="op.color">

2
packages/nc-gui/components/cell/SingleSelect.vue

@ -86,7 +86,7 @@ watch(isOpen, (n, _o) => {
v-for="op of options"
:key="op.title"
:value="op.title"
:data-nc="`select-option-${column.title}-${rowIndex}`"
:data-testid="`select-option-${column.title}-${rowIndex}`"
@click.stop
>
<a-tag class="rounded-tag" :color="op.color">

2
packages/nc-gui/components/cell/attachment/index.vue

@ -161,7 +161,7 @@ watch(
v-if="!isReadonly"
:class="{ 'mx-auto px-4': !visibleItems.length }"
class="group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-1 p-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
data-nc="attachment-cell-file-picker-button"
data-testid="attachment-cell-file-picker-button"
@click.stop="open"
>
<MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />

8
packages/nc-gui/components/dashboard/TreeView.vue

@ -331,13 +331,13 @@ const onSearchCloseIconClick = () => {
class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
:data-nc="`tree-view-table-${table.title}`"
:data-testid="`tree-view-table-${table.title}`"
@click="addTableTab(table)"
>
<GeneralTooltip class="pl-5 pr-3 py-2" modifier-key="Alt">
<template #title>{{ table.table_name }}</template>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto" :data-nc="`tree-view-table-draggable-handle-${table.title}`">
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<MdiDragVertical
v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`"
@ -366,14 +366,14 @@ const onSearchCloseIconClick = () => {
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table)">
<div class="nc-project-menu-item" :data-nc="`sidebar-table-rename-${table.title}`">
<div class="nc-project-menu-item" :data-testid="`sidebar-table-rename-${table.title}`">
{{ $t('general.rename') }}
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-delete')"
:data-nc="`sidebar-table-delete-${table.title}`"
:data-testid="`sidebar-table-delete-${table.title}`"
@click="deleteTable(table)"
>
<div class="nc-project-menu-item">

2
packages/nc-gui/components/dashboard/settings/AuditTab.vue

@ -112,7 +112,7 @@ const columns = [
:columns="columns"
:pagination="false"
:loading="isLoading"
data-nc="audit-tab-table"
data-testid="audit-tab-table"
>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />

4
packages/nc-gui/components/dashboard/settings/Modal.vue

@ -178,7 +178,7 @@ watch(
<a-button
type="text"
class="!rounded-md border-none -mt-1.5 -mr-1"
data-nc="settings-modal-close-button"
data-testid="settings-modal-close-button"
@click="vModel = false"
>
<template #icon>
@ -220,7 +220,7 @@ watch(
</a-menu-item>
</a-menu>
<component :is="selectedSubTab?.body" class="px-2 py-6" :data-nc="`nc-settings-subtab-${selectedSubTab.title}`" />
<component :is="selectedSubTab?.body" class="px-2 py-6" :data-testid="`nc-settings-subtab-${selectedSubTab.title}`" />
</a-layout-content>
</a-layout>
</a-modal>

2
packages/nc-gui/components/dlg/TableCreate.vue

@ -105,7 +105,7 @@ onMounted(() => {
v-model:value="table.title"
size="large"
hide-details
data-nc="create-table-title-input"
data-testid="create-table-title-input"
:placeholder="$t('msg.info.enterTableName')"
/>
</a-form-item>

2
packages/nc-gui/components/general/TruncateText.vue

@ -38,7 +38,7 @@ const shortName = computed(() =>
</template>
<div class="w-full">{{ shortName }}</div>
</a-tooltip>
<div v-else class="w-full" data-nc="truncate-label">
<div v-else class="w-full" data-testid="truncate-label">
<slot />
</div>
<div ref="text" class="hidden">

46
packages/nc-gui/components/smartsheet/Form.vue

@ -387,7 +387,7 @@ watch(view, (nextView) => {
</script>
<template>
<a-row v-if="submitted" class="h-full" data-nc="nc-form-wrapper-submit">
<a-row v-if="submitted" class="h-full" data-testid="nc-form-wrapper-submit">
<a-col :span="24">
<div v-if="formViewData" class="items-center justify-center text-center mt-2">
<a-alert type="success">
@ -407,7 +407,7 @@ watch(view, (nextView) => {
</a-col>
</a-row>
<a-row v-else class="h-full flex" data-nc="nc-form-wrapper">
<a-row v-else class="h-full flex" data-testid="nc-form-wrapper">
<a-col v-if="isEditable" :span="8" class="shadow p-2 md:p-4 h-full overflow-auto scrollbar-thin-dull nc-form-left-drawer">
<div class="flex flex-wrap gap-2">
<div class="flex-1 text-lg">
@ -419,7 +419,7 @@ watch(view, (nextView) => {
v-if="hiddenColumns.length"
type="button"
class="nc-form-add-all color-transition bg-white transform hover:(text-primary ring ring-accent ring-opacity-100) active:translate-y-[1px] px-2 py-1 shadow-md rounded"
data-nc="nc-form-add-all"
data-testid="nc-form-add-all"
@click="addAllColumns"
>
<!-- Add all -->
@ -430,7 +430,7 @@ watch(view, (nextView) => {
v-if="localColumns.length"
type="button"
class="nc-form-remove-all color-transition bg-white transform hover:(text-primary ring ring-accent ring-opacity-100) active:translate-y-[1px] px-2 py-1 shadow-md rounded"
data-nc="nc-form-remove-all"
data-testid="nc-form-remove-all"
@click="removeAllColumns"
>
<!-- Remove all -->
@ -452,7 +452,7 @@ watch(view, (nextView) => {
<a-card
size="small"
class="!border-0 color-transition cursor-pointer item hover:(bg-primary ring-1 ring-accent ring-opacity-100) bg-opacity-10 !rounded !shadow-lg"
:data-nc="`nc-form-hidden-column-${element.label || element.title}`"
:data-testid="`nc-form-hidden-column-${element.label || element.title}`"
@mousedown="moved = false"
@mousemove="moved = false"
@mouseup="handleMouseUp(element, index)"
@ -480,7 +480,7 @@ watch(view, (nextView) => {
<template #footer>
<div
class="my-4 select-none border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center nc-drag-n-drop-to-hide"
data-nc="nc-drag-n-drop-to-hide"
data-testid="nc-drag-n-drop-to-hide"
>
<!-- Drag and drop fields here to hide -->
{{ $t('msg.info.dragDropHide') }}
@ -538,7 +538,7 @@ watch(view, (nextView) => {
hide-details
placeholder="Form Title"
:bordered="false"
data-nc="nc-form-heading"
data-testid="nc-form-heading"
@blur="updateView"
@keydown.enter="updateView"
/>
@ -558,7 +558,7 @@ watch(view, (nextView) => {
:placeholder="$t('msg.info.formDesc')"
:bordered="false"
:disabled="!isEditable"
data-nc="nc-form-sub-heading"
data-testid="nc-form-sub-heading"
@blur="updateView"
@click="updateView"
/>
@ -588,7 +588,7 @@ watch(view, (nextView) => {
'bg-primary bg-opacity-5 ring-0.5 ring-accent ring-opacity-100': activeRow === element.title,
},
]"
data-nc="nc-form-fields"
data-testid="nc-form-fields"
@click="activeRow = element.title"
>
<div
@ -597,7 +597,7 @@ watch(view, (nextView) => {
>
<MdiEyeOffOutline
class="opacity-0 nc-field-remove-icon"
data-nc="nc-field-remove-icon"
data-testid="nc-field-remove-icon"
@click.stop="hideColumn(index)"
/>
</div>
@ -606,7 +606,7 @@ watch(view, (nextView) => {
<div class="flex gap-2 items-center">
<span
class="text-gray-500 mr-2 nc-form-input-required"
data-nc="nc-form-input-required"
data-testid="nc-form-input-required"
@click="
() => {
element.required = !element.required
@ -630,7 +630,7 @@ watch(view, (nextView) => {
v-model:value="element.label"
type="text"
class="form-meta-input nc-form-input-label"
data-nc="nc-form-input-label"
data-testid="nc-form-input-label"
:placeholder="$t('msg.info.formInput')"
@change="updateColMeta(element)"
>
@ -642,7 +642,7 @@ watch(view, (nextView) => {
v-model:value="element.description"
type="text"
class="form-meta-input text-sm nc-form-input-help-text"
data-nc="nc-form-input-help-text"
data-testid="nc-form-input-help-text"
:placeholder="$t('msg.info.formHelpText')"
@change="updateColMeta(element)"
/>
@ -655,7 +655,7 @@ watch(view, (nextView) => {
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
data-nc="nc-form-input-label"
data-testid="nc-form-input-label"
/>
<LazySmartsheetHeaderCell
@ -663,7 +663,7 @@ watch(view, (nextView) => {
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
data-nc="nc-form-input-label"
data-testid="nc-form-input-label"
/>
</div>
@ -678,7 +678,7 @@ watch(view, (nextView) => {
:row="row"
class="nc-input"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-nc="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"
@click.stop.prevent
/>
@ -694,14 +694,14 @@ watch(view, (nextView) => {
v-model="formState[element.title]"
class="nc-input"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-nc="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"
:edit-enabled="true"
@click.stop.prevent
/>
</a-form-item>
<div class="text-gray-500 text-xs" data-nc="nc-form-input-help-text-label">{{ element.description }}</div>
<div class="text-gray-500 text-xs" data-testid="nc-form-input-help-text-label">{{ element.description }}</div>
</div>
</template>
@ -716,7 +716,7 @@ watch(view, (nextView) => {
</Draggable>
<div class="justify-center flex mt-6">
<button type="submit" class="uppercase scaling-btn nc-form-submit" data-nc="nc-form-submit" @click="submitForm">
<button type="submit" class="uppercase scaling-btn nc-form-submit" data-testid="nc-form-submit" @click="submitForm">
{{ $t('general.submit') }}
</button>
</div>
@ -738,7 +738,7 @@ watch(view, (nextView) => {
:rows="3"
hide-details
class="nc-form-after-submit-msg"
data-nc="nc-form-after-submit-msg"
data-testid="nc-form-after-submit-msg"
@change="updateView"
/>
@ -751,7 +751,7 @@ watch(view, (nextView) => {
v-e="[`a:form-view:submit-another-form`]"
size="small"
class="nc-form-checkbox-submit-another-form"
data-nc="nc-form-checkbox-submit-another-form"
data-testid="nc-form-checkbox-submit-another-form"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.submitAnotherForm') }}</span>
@ -764,7 +764,7 @@ watch(view, (nextView) => {
v-e="[`a:form-view:show-blank-form`]"
size="small"
class="nc-form-checkbox-show-blank-form"
data-nc="nc-form-checkbox-show-blank-form"
data-testid="nc-form-checkbox-show-blank-form"
@change="updateView"
/>
@ -777,7 +777,7 @@ watch(view, (nextView) => {
v-e="[`a:form-view:email-me`]"
size="small"
class="nc-form-checkbox-send-email"
data-nc="nc-form-checkbox-send-email"
data-testid="nc-form-checkbox-send-email"
@change="onEmailChange"
/>

4
packages/nc-gui/components/smartsheet/Gallery.vue

@ -163,14 +163,14 @@ watch(view, async (nextView) => {
</script>
<template>
<div class="flex flex-col h-full w-full overflow-auto nc-gallery" data-nc="nc-gallery-wrapper">
<div class="flex flex-col h-full w-full overflow-auto nc-gallery" data-testid="nc-gallery-wrapper">
<div class="nc-gallery-container grid gap-2 my-4 px-3">
<div v-for="record in data" :key="`record-${record.row.id}`">
<LazySmartsheetRow :row="record">
<a-card
hoverable
class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]"
:data-nc="`nc-gallery-card-${record.row.id}`"
:data-testid="`nc-gallery-card-${record.row.id}`"
@click="expandFormClick($event, record)"
>
<template v-if="galleryData?.fk_cover_image_col_id" #cover>

22
packages/nc-gui/components/smartsheet/Grid.vue

@ -460,8 +460,8 @@ watch(
</script>
<template>
<div class="relative flex flex-col h-full min-h-0 w-full" data-nc="nc-grid-wrapper">
<general-overlay :model-value="isLoading" inline transition class="!bg-opacity-15" data-nc="grid-load-spinner">
<div class="relative flex flex-col h-full min-h-0 w-full" data-testid="nc-grid-wrapper">
<general-overlay :model-value="isLoading" inline transition class="!bg-opacity-15" data-testid="grid-load-spinner">
<div class="flex items-center justify-center h-full w-full !bg-white !bg-opacity-85 z-1000">
<a-spin size="large" />
</div>
@ -480,8 +480,8 @@ watch(
>
<thead ref="tableHead">
<tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px]">
<th data-nc="grid-id-column">
<div class="w-full h-full bg-gray-100 flex min-w-[70px] pl-5 pr-1 items-center" data-nc="nc-check-all">
<th data-testid="grid-id-column">
<div class="w-full h-full bg-gray-100 flex min-w-[70px] pl-5 pr-1 items-center" data-testid="nc-check-all">
<template v-if="!readOnly">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
<div
@ -546,8 +546,8 @@ watch(
<tbody ref="tbodyEl" @selectstart.prevent>
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr class="nc-grid-row" :data-nc="`grid-row-${rowIndex}`">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-nc="`cell-Id-${rowIndex}`">
<tr class="nc-grid-row" :data-testid="`grid-row-${rowIndex}`">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`">
<div class="items-center flex gap-1 min-w-[55px]">
<div
v-if="!readOnly || !isLocked"
@ -568,10 +568,14 @@ watch(
<div
v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)"
class="nc-expand"
:data-nc="`nc-expand-${rowIndex}`"
:data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }"
>
<a-spin v-if="row.rowMeta.saving" class="!flex items-center" :data-nc="`row-save-spinner-${rowIndex}`" />
<a-spin
v-if="row.rowMeta.saving"
class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`"
/>
<template v-else>
<span
v-if="row.rowMeta?.commentCount"
@ -605,7 +609,7 @@ watch(
(hasEditPermission && selectedRange(rowIndex, colIndex)),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row),
}"
:data-nc="`cell-${columnObj.title}-${rowIndex}`"
:data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-key="rowIndex + columnObj.id"
:data-col="columnObj.id"
:data-title="columnObj.title"

2
packages/nc-gui/components/smartsheet/Kanban.vue

@ -312,7 +312,7 @@ watch(view, async (nextView) => {
</script>
<template>
<div class="flex h-full bg-white px-2" data-nc="nc-kanban-wrapper">
<div class="flex h-full bg-white px-2" data-testid="nc-kanban-wrapper">
<div ref="kanbanContainerRef" class="nc-kanban-container flex my-4 px-3 overflow-x-scroll overflow-y-hidden">
<a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']" overlay-class-name="nc-dropdown-kanban-context-menu">
<!-- Draggable Stack -->

2
packages/nc-gui/components/smartsheet/Pagination.vue

@ -19,7 +19,7 @@ const page = computed({
<template>
<div class="flex items-center mb-1">
<span v-if="count !== null && count !== Infinity" class="caption ml-5 text-gray-500" data-nc="grid-pagination">
<span v-if="count !== null && count !== Infinity" class="caption ml-5 text-gray-500" data-testid="grid-pagination">
{{ count }} {{ count !== 1 ? $t('objects.records') : $t('objects.record') }}
</span>

2
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -124,7 +124,7 @@ onMounted(() => {
:class="{ '!w-[600px]': formState.uidt === UITypes.Formula }"
@click.stop
>
<a-form v-model="formState" no-style name="column-create-or-edit" layout="vertical" data-nc="add-or-edit-column">
<a-form v-model="formState" no-style name="column-create-or-edit" layout="vertical" data-testid="add-or-edit-column">
<div class="flex flex-col gap-2">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
<a-input

6
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -137,7 +137,7 @@ watch(inputs, () => {
v-if="!isKanban"
small
class="nc-child-draggable-icon handle"
:data-nc="`select-option-column-handle-icon-${element.title}`"
:data-testid="`select-option-column-handle-icon-${element.title}`"
/>
<a-dropdown
v-model:visible="colorMenus[index]"
@ -162,14 +162,14 @@ watch(inputs, () => {
ref="inputs"
v-model:value="element.title"
class="caption"
:data-nc="`select-column-option-input-${index}`"
:data-testid="`select-column-option-input-${index}`"
@change="optionChanged(element.id)"
/>
<MdiClose
class="ml-2 hover:!text-black"
:style="{ color: 'red' }"
:data-nc="`select-column-option-remove-${index}`"
:data-testid="`select-column-option-remove-${index}`"
@click="removeOption(index)"
/>
</div>

2
packages/nc-gui/components/smartsheet/expanded-form/Header.vue

@ -45,9 +45,11 @@ const { copy } = useClipboard()
const copyRecordUrl = () => {
copy(
encodeURI(
`${dashboardUrl?.value}#/${route.params.projectType}/${route.params.projectId}/${route.params.type}/${meta.value?.title}${
props.view ? `/${props.view.title}` : ''
}?rowId=${primaryKey.value}`,
),
)
message.success('Copied to clipboard')
}

2
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -150,7 +150,7 @@ export default {
:key="col.title"
class="mt-2 py-2"
:class="`nc-expand-col-${col.title}`"
:data-nc="`nc-expand-col-${col.title}`"
:data-testid="`nc-expand-col-${col.title}`"
>
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />

8
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -167,12 +167,12 @@ function onStopEdit() {
<template>
<a-menu-item
class="select-none group !flex !items-center !my-0 hover:(bg-primary !bg-opacity-5)"
:data-nc="`view-sidebar-view-${vModel.alias || vModel.title}`"
:data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`"
@dblclick.stop="onDblClick"
@click.stop="onClick"
>
<div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2" data-nc="view-item">
<div class="flex w-auto" :data-nc="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2" data-testid="view-item">
<div class="flex w-auto" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<MdiDrag
class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 !cursor-move"
@click.stop.prevent
@ -194,7 +194,7 @@ function onStopEdit() {
<div class="flex-1" />
<template v-if="!isEditing && !isLocked && isUIAllowed('virtualViewsCreateOrEdit')">
<div class="flex items-center gap-1" :data-nc="`view-sidebar-view-actions-${vModel.alias || vModel.title}`">
<div class="flex items-center gap-1" :data-testid="`view-sidebar-view-actions-${vModel.alias || vModel.title}`">
<a-tooltip placement="left">
<template #title>
{{ $t('activity.copyView') }}

2
packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue

@ -82,7 +82,7 @@ const filterAutoSaveLoc = computed({
ref="filterComp"
class="nc-table-toolbar-menu shadow-lg"
:auto-save="filterAutoSave"
data-nc="nc-filter-menu"
data-testid="nc-filter-menu"
@update:filters-length="filtersLength = $event"
>
<div v-if="!isPublic" class="flex items-end mt-2 min-h-[30px]" @click.stop>

4
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -139,7 +139,7 @@ const getIcon = (c: ColumnType) =>
<template #overlay>
<div
class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto !border"
data-nc="nc-fields-menu"
data-testid="nc-fields-menu"
@click.stop
>
<a-card
@ -167,7 +167,7 @@ const getIcon = (c: ColumnType) =>
v-show="filteredFieldList.includes(field)"
:key="field.id"
class="px-2 py-1 flex items-center"
:data-nc="`nc-fields-menu-${field.title}`"
:data-testid="`nc-fields-menu-${field.title}`"
@click.stop
>
<a-checkbox

26
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -212,8 +212,7 @@ watch(passwordProtected, (value) => {
wrap-class-name="nc-modal-share-view"
>
<div
data-cy="nc-modal-share-view__link"
data-nc="nc-modal-share-view__link"
data-testid="nc-modal-share-view__link"
class="share-link-box !bg-primary !bg-opacity-5 ring-1 ring-accent ring-opacity-100"
>
<div class="flex-1 h-min text-xs">{{ sharedViewUrl }}</div>
@ -235,8 +234,7 @@ watch(passwordProtected, (value) => {
<a-checkbox
v-if="shared.type === ViewTypes.FORM"
v-model:checked="surveyMode"
data-cy="nc-modal-share-view__survey-mode"
data-nc="nc-modal-share-view__survey-mode"
data-testid="nc-modal-share-view__survey-mode"
class="!text-sm"
>
Use Survey Mode
@ -250,7 +248,7 @@ watch(passwordProtected, (value) => {
</template>
<a-input
v-model:value="transitionDuration"
data-cy="nc-form-signin__email"
data-testid="nc-form-signin__email"
size="small"
class="!w-32"
type="number"
@ -266,8 +264,7 @@ watch(passwordProtected, (value) => {
<a-checkbox
v-if="shared.type === ViewTypes.FORM"
v-model:checked="viewTheme"
data-cy="nc-modal-share-view__with-theme"
data-nc="nc-modal-share-view__with-theme"
data-testid="nc-modal-share-view__with-theme"
class="!text-sm"
>
Use Theme
@ -276,13 +273,12 @@ watch(passwordProtected, (value) => {
<Transition name="layout" mode="out-in">
<div v-if="viewTheme" class="flex pl-6">
<LazyGeneralColorPicker
data-cy="nc-modal-share-view__theme-picker"
data-testid="nc-modal-share-view__theme-picker"
class="!p-0"
:model-value="shared.meta.theme?.primaryColor"
:colors="projectThemeColors"
:row-size="9"
:advanced="false"
data-nc="nc-modal-share-view__theme-picker"
@input="onChangeTheme"
/>
</div>
@ -293,9 +289,8 @@ watch(passwordProtected, (value) => {
<!-- Password Protection -->
<a-checkbox
v-model:checked="passwordProtected"
data-cy="nc-modal-share-view__with-password"
data-testid="nc-modal-share-view__with-password"
class="!text-sm !my-1"
data-nc="nc-modal-share-view__with-password"
>
{{ $t('msg.info.beforeEnablePwd') }}
</a-checkbox>
@ -304,8 +299,7 @@ watch(passwordProtected, (value) => {
<div v-if="passwordProtected" class="pl-6 flex gap-2 mt-2 mb-4">
<a-input
v-model:value="shared.password"
data-cy="nc-modal-share-view__password"
data-nc="nc-modal-share-view__password"
data-testid="nc-modal-share-view__password"
size="small"
class="!text-xs max-w-[250px]"
type="password"
@ -313,8 +307,7 @@ watch(passwordProtected, (value) => {
/>
<a-button
data-cy="nc-modal-share-view__save-password"
data-nc="nc-modal-share-view__save-password"
data-testid="nc-modal-share-view__save-password"
size="small"
class="!text-xs"
@click="saveShareLinkPassword"
@ -333,8 +326,7 @@ watch(passwordProtected, (value) => {
(shared.type === ViewTypes.GRID || shared.type === ViewTypes.KANBAN || shared.type === ViewTypes.GALLERY)
"
v-model:checked="allowCSVDownload"
data-cy="nc-modal-share-view__with-csv-download"
data-nc="nc-modal-share-view__with-csv-download"
data-testid="nc-modal-share-view__with-csv-download"
class="!text-sm"
>
{{ $t('labels.downloadAllowed') }}

2
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -55,7 +55,7 @@ watch(
<template #overlay>
<div
class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border"
data-nc="nc-sorts-menu"
data-testid="nc-sorts-menu"
>
<div v-if="sorts?.length" class="sort-grid mb-2" @click.stop>
<template v-for="(sort, i) in sorts || []" :key="i">

2
packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue

@ -103,7 +103,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
</a-button>
<template #overlay>
<a-menu class="ml-6 !text-sm !px-0 !py-2 !rounded" data-nc="toolbar-actions">
<a-menu class="ml-6 !text-sm !px-0 !py-2 !rounded" data-testid="toolbar-actions">
<a-menu-item-group>
<a-sub-menu
v-if="isUIAllowed('view-type')"

2
packages/nc-gui/components/tabs/auth/user-management/ShareBase.vue

@ -136,7 +136,7 @@ onMounted(() => {
</script>
<template>
<div class="flex flex-col w-full" data-nc="nc-share-base-sub-modal">
<div class="flex flex-col w-full" data-testid="nc-share-base-sub-modal">
<div class="flex flex-row items-center space-x-0.5 pl-2 h-[0.8rem]">
<MdiOpenInNew />

13
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -102,14 +102,19 @@ watch(
<div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col py-6">
<div class="flex mb-4 items-center gap-2 px-12">
<div class="flex-1" />
<MdiReload v-if="!isForm" class="cursor-pointer text-gray-500" data-cy="nc-child-list-reload" @click="loadChildrenList" />
<MdiReload
v-if="!isForm"
class="cursor-pointer text-gray-500"
data-testid="nc-child-list-reload"
@click="loadChildrenList"
/>
<a-button
v-if="!readonly"
type="primary"
ghost
class="!text-xs"
data-cy="nc-child-list-button-link-to"
data-testid="nc-child-list-button-link-to"
size="small"
@click="emit('attachRecord')"
>
@ -143,13 +148,13 @@ watch(
<div v-if="!readonly" class="flex gap-2">
<MdiLinkVariantRemove
class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
data-cy="nc-child-list-icon-unlink"
data-testid="nc-child-list-icon-unlink"
@click.stop="unlinkRow(row)"
/>
<MdiDeleteOutline
v-if="!readonly && !isPublic"
class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
data-cy="nc-child-list-icon-delete"
data-testid="nc-child-list-icon-delete"
@click.stop="deleteRelatedRow(row, unlinkIfNewRow)"
/>
</div>

4
packages/nc-gui/composables/useColumnCreateStore.ts

@ -193,6 +193,10 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
formState.value.table_name = meta.value?.table_name
// formState.value.title = formState.value.column_name
if (column.value) {
// reset column validation if column is not to be validated
if (!columnToValidate.includes(formState.value.uidt)) {
formState.value.validate = ''
}
await $api.dbTableColumn.update(column.value?.id as string, formState.value)
// Column updated
message.success(t('msg.success.columnUpdated'))

8
packages/nc-gui/layouts/base.vue

@ -44,7 +44,7 @@ hooks.hook('page:finish', () => {
<div
v-if="!route.params.projectType"
v-e="['c:navbar:home']"
data-cy="nc-noco-brand-icon"
data-testid="nc-noco-brand-icon"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
>
@ -57,7 +57,7 @@ hooks.hook('page:finish', () => {
</div>
<div class="!text-white flex justify-center">
<div v-show="isLoading" class="flex items-center gap-2 ml-3" data-nc="nc-loading">
<div v-show="isLoading" class="flex items-center gap-2 ml-3" data-testid="nc-loading">
{{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
@ -79,14 +79,14 @@ hooks.hook('page:finish', () => {
<template v-if="signedIn">
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-user-accounts-menu">
<MdiDotsVertical
data-cy="nc-menu-accounts"
data-testid="nc-menu-accounts"
class="md:text-xl cursor-pointer hover:text-accent nc-menu-accounts text-white"
@click.prevent
/>
<template #overlay>
<a-menu class="!py-0 leading-8 !rounded">
<a-menu-item key="0" data-cy="nc-menu-accounts__user-settings" class="!rounded-t">
<a-menu-item key="0" data-testid="nc-menu-accounts__user-settings" class="!rounded-t">
<nuxt-link v-e="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-accent" />&nbsp;

2
packages/nc-gui/layouts/default.vue

@ -20,7 +20,7 @@ export default {
<template>
<div class="w-full h-full">
<Teleport :to="hasSidebar ? '#nc-sidebar-left' : null" :disabled="!hasSidebar">
<slot name="sidebar" />
<slot :key="$route.name" name="sidebar" />
</Teleport>
<a-layout-content>

2
packages/nc-gui/layouts/shared-view.vue

@ -53,7 +53,7 @@ export default {
<div class="flex justify-center items-center">
<div class="flex items-center gap-2 ml-3 text-white">
<template v-if="isLoading">
<span class="text-white" data-nc="nc-loading">{{ $t('general.loading') }}</span>
<span class="text-white" data-testid="nc-loading">{{ $t('general.loading') }}</span>
<MdiReload :class="{ 'animate-infinite animate-spin ': isLoading }" />
</template>

2
packages/nc-gui/middleware/auth.global.ts

@ -37,7 +37,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
const { allRoles } = useRoles()
/** if user isn't signed in and google auth is enabled, try to check if sign-in data is present */
if (!state.signedIn && state.appInfo.value.googleAuthEnabled) await tryGoogleAuth(api, state.signIn)
if (!state.signedIn.value && state.appInfo.value.googleAuthEnabled) await tryGoogleAuth(api, state.signIn)
/** if public allow all visitors */
if (to.meta.public) return

4
packages/nc-gui/pages/[projectType]/[projectId]/index.vue

@ -194,7 +194,7 @@ onBeforeUnmount(reset)
<div
v-if="isOpen && !isSharedBase"
v-e="['c:navbar:home']"
data-cy="nc-noco-brand-icon"
data-testid="nc-noco-brand-icon"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
>
@ -229,7 +229,7 @@ onBeforeUnmount(reset)
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
data-nc="nc-project-menu"
data-testid="nc-project-menu"
class="group cursor-pointer flex gap-1 items-center nc-project-menu overflow-hidden"
>
<template v-if="isOpen">

6
packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue

@ -52,14 +52,14 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
</div>
<a-tooltip v-if="tab.title?.length > 12" placement="bottom">
<div class="truncate" :data-nc="`nc-root-tabs-${tab.title}`">{{ tab.title }}</div>
<div class="truncate" :data-testid="`nc-root-tabs-${tab.title}`">{{ tab.title }}</div>
<template #title>
<div>{{ tab.title }}</div>
</template>
</a-tooltip>
<div v-else :data-nc="`nc-root-tabs-${tab.title}`">{{ tab.title }}</div>
<div v-else :data-testid="`nc-root-tabs-${tab.title}`">{{ tab.title }}</div>
</div>
</template>
</a-tab-pane>
@ -68,7 +68,7 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
<span class="flex-1" />
<div class="flex justify-center self-center mr-2 min-w-[115px]">
<div v-show="isLoading" class="flex items-center gap-2 ml-3 text-gray-200" data-nc="nc-loading">
<div v-show="isLoading" class="flex items-center gap-2 ml-3 text-gray-200" data-testid="nc-loading">
{{ $t('general.loading') }}
<MdiLoading class="animate-infinite animate-spin" />

6
packages/nc-gui/pages/[projectType]/form/[viewId]/index/index.vue

@ -85,7 +85,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
<LazySmartsheetVirtualCell
v-if="isVirtualCol(field)"
class="mt-0 nc-input"
:data-nc="`nc-form-input-cell-${field.label || field.title}`"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title.replaceAll(' ', '')}`"
:column="field"
/>
@ -94,7 +94,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
v-else
v-model="formState[field.title]"
class="nc-input"
:data-nc="`nc-form-input-cell-${field.label || field.title}`"
:data-testid="`nc-form-input-cell-${field.label || field.title}`"
:class="`nc-form-input-${field.title.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="true"
@ -115,7 +115,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
<button
type="submit"
class="uppercase scaling-btn prose-sm"
data-nc="shared-form-submit-button"
data-testid="shared-form-submit-button"
@click="submitForm"
>
{{ $t('general.submit') }}

43
packages/nc-gui/pages/[projectType]/form/[viewId]/index/survey.vue

@ -208,15 +208,14 @@ onMounted(() => {
class="max-w-[max(33%,600px)] mx-auto flex flex-col justify-end"
>
<div class="px-4 md:px-0 flex flex-col justify-end">
<h1 class="prose-2xl font-bold self-center my-4" data-cy="nc-survey-form__heading" data-nc="nc-survey-form__heading">
<h1 class="prose-2xl font-bold self-center my-4" data-testid="nc-survey-form__heading">
{{ sharedFormView.heading }}
</h1>
<h2
v-if="sharedFormView.subheading && sharedFormView.subheading !== ''"
class="prose-lg text-slate-500 dark:text-slate-300 self-center mb-4 leading-6"
data-cy="nc-survey-form__sub-heading"
data-nc="nc-survey-form__sub-heading"
data-testid="nc-survey-form__sub-heading"
>
{{ sharedFormView?.subheading }}
</h2>
@ -231,7 +230,7 @@ onMounted(() => {
class="color-transition h-full flex flex-col mt-6 gap-4 w-full max-w-[max(33%,600px)] m-auto"
>
<div v-if="field && !submitted" class="flex flex-col gap-2">
<div class="flex nc-form-column-label" data-nc="nc-form-column-label">
<div class="flex nc-form-column-label" data-testid="nc-form-column-label">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
:column="{ ...field, title: field.label || field.title }"
@ -254,8 +253,7 @@ onMounted(() => {
v-model="formState[field.title]"
class="mt-0 nc-input"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:data-cy="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:data-nc="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
/>
@ -263,8 +261,7 @@ onMounted(() => {
v-else
v-model="formState[field.title]"
class="nc-input"
:data-cy="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:data-nc="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:data-testid="`nc-survey-form__input-${field.title.replaceAll(' ', '')}`"
:column="field"
:edit-enabled="true"
/>
@ -277,8 +274,7 @@ onMounted(() => {
<div
class="block text-[14px]"
:class="field.uidt === UITypes.Checkbox ? 'text-center' : ''"
data-nc="nc-survey-form__field-description"
data-cy="nc-survey-form__field-description"
data-testid="nc-survey-form__field-description"
>
{{ field.description }}
</div>
@ -302,8 +298,7 @@ onMounted(() => {
"
type="submit"
class="uppercase scaling-btn prose-sm"
data-cy="nc-survey-form__btn-submit"
data-nc="nc-survey-form__btn-submit"
data-testid="nc-survey-form__btn-submit"
@click="submit"
>
{{ $t('general.submit') }}
@ -318,8 +313,7 @@ onMounted(() => {
>
<button
class="bg-opacity-100 scaling-btn flex items-center gap-1"
data-cy="nc-survey-form__btn-next"
data-nc="nc-survey-form__btn-next"
data-testid="nc-survey-form__btn-next"
:class="[
v$.localState[field.title]?.$error ? 'after:!bg-gray-100 after:!ring-red-500' : '',
animationTarget === AnimationTarget.OkButton && isAnimating
@ -349,11 +343,7 @@ onMounted(() => {
<Transition name="slide-left">
<div v-if="submitted" class="flex flex-col justify-center items-center text-center">
<div
class="text-lg px-6 py-3 bg-green-300 text-gray-700 rounded"
data-cy="nc-survey-form__success-msg"
data-nc="nc-survey-form__success-msg"
>
<div class="text-lg px-6 py-3 bg-green-300 text-gray-700 rounded" data-testid="nc-survey-form__success-msg">
<template v-if="sharedFormView?.success_msg">
{{ sharedFormView?.success_msg }}
</template>
@ -376,8 +366,7 @@ onMounted(() => {
<button
type="button"
class="scaling-btn bg-opacity-100"
data-cy="nc-survey-form__btn-submit-another-form"
data-nc="nc-survey-form__btn-submit-another-form"
data-testid="nc-survey-form__btn-submit-another-form"
@click="resetForm"
>
Submit Another Form
@ -391,11 +380,7 @@ onMounted(() => {
</div>
<template v-if="!submitted">
<div
class="mb-24 md:my-4 select-none text-center text-gray-500 dark:text-slate-200"
data-cy="nc-survey-form__footer"
data-nc="nc-survey-form__footer"
>
<div class="mb-24 md:my-4 select-none text-center text-gray-500 dark:text-slate-200" data-testid="nc-survey-form__footer">
{{ index + 1 }} / {{ formColumns?.length }}
</div>
</template>
@ -414,8 +399,7 @@ onMounted(() => {
: ''
"
class="p-0.5 flex items-center group color-transition"
data-cy="nc-survey-form__icon-prev"
data-nc="nc-survey-form__icon-prev"
data-testid="nc-survey-form__icon-prev"
@click="goPrevious()"
>
<MdiChevronLeft :class="isFirst ? 'text-gray-300' : 'group-hover:text-accent'" class="text-2xl md:text-md" />
@ -434,8 +418,7 @@ onMounted(() => {
: ''
"
class="p-0.5 flex items-center group color-transition"
data-cy="nc-survey-form__icon-next"
data-nc="nc-survey-form__icon-next"
data-testid="nc-survey-form__icon-next"
@click="goNext()"
>
<MdiChevronRight

6
packages/nc-gui/pages/index/index/index.vue

@ -144,7 +144,7 @@ const copyProjectMeta = async () => {
<template>
<div
class="relative flex flex-col justify-center gap-2 w-full p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)"
data-nc="projects-container"
data-testid="projects-container"
>
<h1 class="flex items-center justify-center gap-2 leading-8 mb-8 mt-4">
<span class="text-4xl nc-project-page-title" @dblclick="copyProjectMeta">{{ $t('title.myProject') }}</span>
@ -166,7 +166,7 @@ const copyProjectMeta = async () => {
v-e="['a:project:refresh']"
class="text-xl text-gray-500 group-hover:text-accent cursor-pointer"
:class="isLoading ? '!text-primary' : ''"
data-nc="projects-reload-button"
data-testid="projects-reload-button"
@click="loadProjects"
/>
</div>
@ -290,7 +290,7 @@ const copyProjectMeta = async () => {
<MdiDeleteOutline
class="nc-action-btn"
:data-nc="`delete-project-${record.title}`"
:data-testid="`delete-project-${record.title}`"
@click.stop="deleteProject(record)"
/>
</div>

12
packages/nc-gui/pages/index/index/user.vue

@ -82,7 +82,7 @@ const resetError = () => {
<a-form
ref="formValidator"
data-cy="nc-user-settings-form"
data-testid="nc-user-settings-form"
layout="vertical"
class="change-password lg:max-w-3/4 w-full !mx-auto"
no-style
@ -91,7 +91,7 @@ const resetError = () => {
>
<Transition name="layout">
<div v-if="error" class="mx-auto mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div data-cy="nc-user-settings-form__error" class="flex items-center gap-2 justify-center">
<div data-testid="nc-user-settings-form__error" class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
{{ error }}
</div>
@ -101,7 +101,7 @@ const resetError = () => {
<a-form-item :label="$t('placeholder.password.current')" name="currentPassword" :rules="formRules.currentPassword">
<a-input-password
v-model:value="form.currentPassword"
data-cy="nc-user-settings-form__current-password"
data-testid="nc-user-settings-form__current-password"
size="large"
class="password"
:placeholder="$t('placeholder.password.current')"
@ -112,7 +112,7 @@ const resetError = () => {
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
data-cy="nc-user-settings-form__new-password"
data-testid="nc-user-settings-form__new-password"
size="large"
class="password"
:placeholder="$t('placeholder.password.new')"
@ -123,7 +123,7 @@ const resetError = () => {
<a-form-item :label="$t('placeholder.password.confirm')" name="passwordRepeat" :rules="formRules.passwordRepeat">
<a-input-password
v-model:value="form.passwordRepeat"
data-cy="nc-user-settings-form__new-password-repeat"
data-testid="nc-user-settings-form__new-password-repeat"
size="large"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@ -132,7 +132,7 @@ const resetError = () => {
</a-form-item>
<div class="text-center">
<button data-cy="nc-user-settings-form__submit" class="scaling-btn bg-opacity-100" type="submit">
<button data-testid="nc-user-settings-form__submit" class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<MdiKeyChange />
{{ $t('activity.changePwd') }}

15
packages/nc-gui/pages/signin.vue

@ -64,7 +64,7 @@ function resetError() {
<template>
<NuxtLayout>
<div
data-cy="nc-form-signin"
data-testid="nc-form-signin"
class="md:bg-primary bg-opacity-5 signin h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signin"
>
<div
@ -87,8 +87,7 @@ function resetError() {
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input
v-model:value="form.email"
data-cy="nc-form-signin__email"
data-nc="nc-form-signin__email"
data-testid="nc-form-signin__email"
size="large"
:placeholder="$t('msg.info.signUp.workEmail')"
@focus="resetError"
@ -98,8 +97,7 @@ function resetError() {
<a-form-item :label="$t('labels.password')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
data-cy="nc-form-signin__password"
data-nc="nc-form-signin__password"
data-testid="nc-form-signin__password"
size="large"
class="password"
:placeholder="$t('msg.info.signUp.enterPassword')"
@ -114,12 +112,7 @@ function resetError() {
</div>
<div class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center">
<button
data-cy="nc-form-signin__submit"
data-nc="nc-form-signin__submit"
class="scaling-btn bg-opacity-100"
type="submit"
>
<button data-testid="nc-form-signin__submit" class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<MdiLogin />
{{ $t('general.signIn') }}

79
packages/nc-gui/tests/playwright/README.md

@ -1,79 +0,0 @@
# Playwright E2E tests
## Setup
Make sure to install the dependencies(in the playwright folder):
```bash
npm install
npx playwright install chromium --with-deps
```
## Run Test Server
Start the backend test server (in `packages/nocodb` folder):
```bash
npm run watch:run:playwright:quick
```
Start the frontend test server (in `packages/nc-gui` folder):
```bash
NUXT_PAGE_TRANSITION_DISABLE=true npm run dev
```
## Running Tests
### Running all tests
For selecting db type, rename `.env.example` to `.env` and set `E2E_DEV_DB_TYPE` to `sqlite`(default), `mysql` or `pg`.
```bash
npm run test
```
### Running individual tests
Add `.only` to the test you want to run:
```js
test.only('should login', async ({ page }) => {
// ...
})
```
```bash
npm run test
```
## Developing tests
### WebStorm
In Webstorm, you can use the `test-debug` run action to run the tests.
Add `.only` to the test you want to run. This will open the test in a chromium session and you can also add break points.
i.e `test.only('should login', async ({ page }) => {`
### VSCode
In VSCode, use this [https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chromium](extension).
It will have run button beside each test in the file.
### Page Objects
Page object is a class which has methods to interact with a page/component. Methods should be thin and should not do a whole lot. They should also be reusable.
All the action methods i.e click of a page object is also responsible for waiting till the action is completed. This can be done by waiting on an API call or some ui change.
Do not add any logic to the tests. Instead, create a page object for the page you are testing.
All the selection, UI actions and assertions should be in the page object.
Page objects should be in `packages/nc-gui/tests/playwright/pages` folder.
### Verify if tests are not flaky
Add `.only` to the added test and run `npm run test:repeat`. This will run the test multiple times and should show if the test is flaky.

5
packages/nocodb/package.json

@ -38,7 +38,7 @@
"watch:run": "cross-env NC_DISABLE_TELE1=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:playwright": "rm -f ./test_noco.db; cross-env DATABASE_URL=sqlite:./test_noco.db PLAYWRIGHT_TEST=true NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/testDocker --log-error --project tsconfig.json\"",
"watch:run:playwright:quick": "rm -f ./test_noco.db; cp ../../scripts/cypress/fixtures/quickTest/noco_0_91_7.db ./test_noco.db; cross-env DATABASE_URL=sqlite:./test_noco.db NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:playwright:pg:cyquick": "rm -f ./test_noco.db; cp ../../scripts/cypress/fixtures/quickTest/noco_0_91_7.db ./test_noco.db; cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG_CyQuick.ts --log-error --project tsconfig.json\"",
"watch:run:playwright:pg:cyquick": "rm -f ./test_noco.db; cp ../../scripts/cypress/fixtures/quickTest/noco_0_91_7.db ./test_noco.db; cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG_CyQuick.ts --log-error --project tsconfig.json\"",
"watch:run:cypress": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/docker --log-error --project tsconfig.json\"",
"watch:run:cypress:pg": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"watch:run:cypress:pg:cyquick": "cross-env EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG_CyQuick.ts --log-error --project tsconfig.json\"",
@ -46,8 +46,7 @@
"watch:run:pg": "cross-env NC_DISABLE_TELE=true EE=true nodemon -e ts,js -w ./src -x \"ts-node src/run/dockerRunPG --log-error --project tsconfig.json\"",
"run": "ts-node src/run/docker",
"watch:try": "nodemon -e ts,js -w ./src -x \"ts-node src/run/try --log-error --project tsconfig.json\"",
"example:docker": "ts-node src/run/docker.ts",
"ci:run": "npm install; npm run watch:run:playwright > mysql_test_backend.log"
"example:docker": "ts-node src/run/docker.ts"
},
"engines": {
"node": ">=8.9"

32
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -97,7 +97,7 @@ class BaseModelSqlv2 {
await this.selectObject({ qb });
qb.where(this.model.primaryKey.column_name, id);
qb.where(_wherePk(this.model.primaryKeys, id));
const data = (await this.extractRawQueryAndExec(qb))?.[0];
@ -1826,12 +1826,16 @@ class BaseModelSqlv2 {
// refer : https://www.sqlite.org/limits.html
const chunkSize = this.isSqlite ? 10 : _chunkSize;
const response = (this.isPg || this.isMssql) ?
await this.dbDriver
const response =
this.isPg || this.isMssql
? await this.dbDriver
.batchInsert(this.model.table_name, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name) :
await this.dbDriver
.batchInsert(this.model.table_name, insertDatas, chunkSize);
.returning(this.model.primaryKey?.column_name)
: await this.dbDriver.batchInsert(
this.model.table_name,
insertDatas,
chunkSize
);
await this.afterBulkInsert(insertDatas, this.dbDriver, cookie);
@ -2236,11 +2240,13 @@ class BaseModelSqlv2 {
for (let i = 0; i < this.model.columns.length; ++i) {
const column = this.model.columns[i];
// skip validation if `validate` is undefined or false
if (!column?.meta?.validate) continue;
if (!column?.meta?.validate && !column?.validate) continue;
const validate = column.getValidators();
const cn = column.column_name;
const columnTitle = column.title;
if (!validate) continue;
const { func, msg } = validate;
for (let j = 0; j < func.length; ++j) {
const fn =
@ -2249,17 +2255,17 @@ class BaseModelSqlv2 {
? customValidators[func[j]]
: Validator[func[j]]
: func[j];
const columnValue = columns?.[cn] || columns?.[columnTitle];
const arg =
typeof func[j] === 'string' ? columns[cn] + '' : columns[cn];
typeof func[j] === 'string' ? columnValue + '' : columnValue;
if (
columns[cn] !== null &&
columns[cn] !== undefined &&
columns[cn] !== '' &&
cn in columns &&
![null, undefined, ''].includes(columnValue) &&
!(fn.constructor.name === 'AsyncFunction' ? await fn(arg) : fn(arg))
) {
NcError.badRequest(
msg[j].replace(/\{VALUE}/g, columns[cn]).replace(/\{cn}/g, cn)
msg[j]
.replace(/\{VALUE}/g, columnValue)
.replace(/\{cn}/g, columnTitle)
);
}
}

8
packages/nocodb/src/lib/plugins/backblaze/Backblaze.ts

@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import AWS from 'aws-sdk';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class Backblaze implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -119,7 +121,7 @@ export default class Backblaze implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

8
packages/nocodb/src/lib/plugins/gcs/Gcs.ts

@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import { Storage, StorageOptions } from '@google-cloud/storage';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class Gcs implements IStorageAdapterV2 {
private storageClient: Storage;
@ -85,7 +87,7 @@ export default class Gcs implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

8
packages/nocodb/src/lib/plugins/linode/LinodeObjectStorage.ts

@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import AWS from 'aws-sdk';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class LinodeObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -109,7 +111,7 @@ export default class LinodeObjectStorage implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

8
packages/nocodb/src/lib/plugins/mino/Minio.ts

@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import { Client as MinioClient } from 'minio';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class Minio implements IStorageAdapterV2 {
private minioClient: MinioClient;
@ -73,7 +75,7 @@ export default class Minio implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

8
packages/nocodb/src/lib/plugins/ovhCloud/OvhCloud.ts

@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import AWS from 'aws-sdk';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class OvhCloud implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -109,7 +111,7 @@ export default class OvhCloud implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

8
packages/nocodb/src/lib/plugins/s3/S3.ts

@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import AWS from 'aws-sdk';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class S3 implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -112,7 +114,7 @@ export default class S3 implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

8
packages/nocodb/src/lib/plugins/scaleway/ScalewayObjectStorage.ts

@ -1,9 +1,11 @@
import path from 'path';
import fs from 'fs';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import AWS from 'aws-sdk';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class ScalewayObjectStorage implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -29,7 +31,7 @@ export default class ScalewayObjectStorage implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

8
packages/nocodb/src/lib/plugins/spaces/Spaces.ts

@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import AWS from 'aws-sdk';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class Spaces implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -117,7 +119,7 @@ export default class Spaces implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

8
packages/nocodb/src/lib/plugins/upcloud/UpoCloud.ts

@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import AWS from 'aws-sdk';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class UpoCloud implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -107,7 +109,7 @@ export default class UpoCloud implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

8
packages/nocodb/src/lib/plugins/vultr/Vultr.ts

@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import AWS from 'aws-sdk';
import { IStorageAdapterV2, XcFile } from 'nc-plugin';
import request from 'request';
import { waitForStreamClose } from '../../utils/pluginUtils';
import {
waitForStreamClose,
generateTempFilePath,
} from '../../utils/pluginUtils';
export default class Vultr implements IStorageAdapterV2 {
private s3Client: AWS.S3;
@ -109,7 +111,7 @@ export default class Vultr implements IStorageAdapterV2 {
public async test(): Promise<boolean> {
try {
const tempFile = path.join(process.cwd(), 'temp.txt');
const tempFile = generateTempFilePath();
const createStream = fs.createWriteStream(tempFile);
await waitForStreamClose(createStream);
await this.fileCreate('nc-test-file.txt', {

6
packages/nocodb/src/lib/utils/pluginUtils.ts

@ -1,4 +1,10 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
export function generateTempFilePath() {
return path.join(os.tmpdir(), 'temp.txt');
}
export async function waitForStreamClose(
stream: fs.WriteStream

0
packages/nc-gui/tests/playwright/.env.example → tests/playwright/.env.example

0
packages/nc-gui/tests/playwright/.eslintrc.json → tests/playwright/.eslintrc.json

1
packages/nc-gui/tests/playwright/.gitignore → tests/playwright/.gitignore vendored

@ -2,6 +2,7 @@ node_modules/
/test-results/
/playwright-report/
/playwright-report copy/
/playwright-report-quick/
/playwright/.cache/
.env
output

0
packages/nc-gui/tests/playwright/.lintstagedrc.json → tests/playwright/.lintstagedrc.json

0
packages/nc-gui/tests/playwright/.prettierignore → tests/playwright/.prettierignore

0
packages/nc-gui/tests/playwright/.prettierrc.js → tests/playwright/.prettierrc.js

100
tests/playwright/README.md

@ -0,0 +1,100 @@
# Playwright E2E tests
## Setup
Make sure to install the dependencies(in the playwright folder, which is `./tests/playwright`):
```bash
npm install
npx playwright install chromium --with-deps
```
## Run Test Server
Start the backend test server (in `packages/nocodb` folder):
```bash
npm run watch:run:playwright
```
Start the frontend test server (in `packages/nc-gui` folder):
```bash
NUXT_PAGE_TRANSITION_DISABLE=true npm run dev
```
## Running Tests
### Running all tests
For selecting db type, rename `.env.example` to `.env` and set `E2E_DEV_DB_TYPE` to `sqlite`(default), `mysql` or `pg`.
headless mode(without opening browser):
```bash
npm run test
```
with browser:
```bash
npm run test:debug
```
</br>
</br>
For setting up mysql:
```bash
docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d
```
For setting up postgres:
```bash
docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml
```
### Running individual tests
Add `.only` to the test you want to run:
```js
test.only('should login', async ({ page }) => {
// ...
})
```
```bash
npm run test
```
## Developing tests
### WebStorm
In Webstorm, you can use the `test-debug` run action to run the tests.
Add `.only` to the test you want to run. This will open the test in a chromium session and you can also add break points.
### VSCode
In VSCode, use this [extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright).
It will have run button beside each test in the file.
## Page Objects
- Page object is a class which has methods to interact with a page/component. Methods should be thin and should not do a whole lot. They should also be reusable.
- All the action methods i.e click of a page object is also responsible for waiting till the action is completed. This can be done by waiting on an API call or some ui change.
- Do not add any logic to the tests. Instead, create a page object for the page you are testing.
All the selection, UI actions and assertions should be in the page object.
Page objects should be in `./tests/playwright/pages` folder.
## Verify if tests are not flaky
Add `.only` to the added tests and run `npm run test:repeat`. This will run the test multiple times and should show if the test is flaky.

0
packages/nc-gui/tests/playwright/constants/index.ts → tests/playwright/constants/index.ts

0
packages/nc-gui/tests/playwright/fixtures/expectedBaseDownloadData.txt → tests/playwright/fixtures/expectedBaseDownloadData.txt

0
packages/nc-gui/tests/playwright/fixtures/expectedBaseDownloadDataPg.txt → tests/playwright/fixtures/expectedBaseDownloadDataPg.txt

0
packages/nc-gui/tests/playwright/fixtures/expectedData.txt → tests/playwright/fixtures/expectedData.txt

0
packages/nc-gui/tests/playwright/fixtures/expectedDataSqlite.txt → tests/playwright/fixtures/expectedDataSqlite.txt

0
packages/nc-gui/tests/playwright/fixtures/sampleFiles/1.json → tests/playwright/fixtures/sampleFiles/1.json

0
packages/nc-gui/tests/playwright/fixtures/sampleFiles/2.json → tests/playwright/fixtures/sampleFiles/2.json

0
packages/nc-gui/tests/playwright/fixtures/sampleFiles/3.json → tests/playwright/fixtures/sampleFiles/3.json

0
packages/nc-gui/tests/playwright/fixtures/sampleFiles/4.json → tests/playwright/fixtures/sampleFiles/4.json

0
packages/nc-gui/tests/playwright/fixtures/sampleFiles/5.json → tests/playwright/fixtures/sampleFiles/5.json

0
packages/nc-gui/tests/playwright/fixtures/sampleFiles/6.json → tests/playwright/fixtures/sampleFiles/6.json

0
packages/nc-gui/tests/playwright/fixtures/sampleFiles/simple.xlsx → tests/playwright/fixtures/sampleFiles/simple.xlsx

0
packages/nc-gui/tests/playwright/fixtures/template.spec.ts → tests/playwright/fixtures/template.spec.ts

0
packages/nc-gui/tests/playwright/package-lock.json → tests/playwright/package-lock.json generated

1
packages/nc-gui/tests/playwright/package.json → tests/playwright/package.json

@ -5,6 +5,7 @@
"main": "index.js",
"scripts": {
"test": "TRACE=true npx playwright test --workers=4",
"test:fast": "npx playwright test --workers=6",
"test:shard:1": "TRACE=true npx playwright test --workers=4 --shard=1/2",
"test:shard:2": "TRACE=true npx playwright test --workers=4 --shard=2/2",
"test:repeat": "TRACE=true npx playwright test --workers=4 --repeat-each=12",

4
packages/nc-gui/tests/playwright/pages/Base.ts → tests/playwright/pages/Base.ts

@ -78,4 +78,8 @@ export default abstract class BasePage {
});
return data as any;
}
async getClipboardText() {
return await this.rootPage.evaluate(() => navigator.clipboard.readText());
}
}

15
packages/nc-gui/tests/playwright/pages/Dashboard/ExpandedForm/index.ts → tests/playwright/pages/Dashboard/ExpandedForm/index.ts

@ -20,6 +20,12 @@ export class ExpandedFormPage extends BasePage {
return this.dashboard.get().locator(`.nc-drawer-expanded-form`);
}
async getShareRowUrl() {
await this.copyUrlButton.click();
await this.verifyToast({ message: 'Copied to clipboard' });
return await this.getClipboardText();
}
async gotoUsingUrlAndRowId({ rowId }: { rowId: string }) {
const url = await this.dashboard.rootPage.url();
const expandedFormUrl = '/' + url.split('/').slice(3).join('/').split('?')[0] + `?rowId=${rowId}`;
@ -28,7 +34,7 @@ export class ExpandedFormPage extends BasePage {
}
async fillField({ columnTitle, value, type = 'text' }: { columnTitle: string; value: string; type?: string }) {
const field = this.get().locator(`[data-nc="nc-expand-col-${columnTitle}"]`);
const field = this.get().locator(`[data-testid="nc-expand-col-${columnTitle}"]`);
await field.hover();
switch (type) {
case 'text':
@ -40,7 +46,7 @@ export class ExpandedFormPage extends BasePage {
break;
case 'hasMany':
case 'manyToMany':
await field.locator(`[data-cy="nc-child-list-button-link-to"]`).click();
await field.locator(`[data-testid="nc-child-list-button-link-to"]`).click();
await this.dashboard.linkRecord.select(value);
break;
}
@ -70,7 +76,7 @@ export class ExpandedFormPage extends BasePage {
await this.get().press('Escape');
await this.get().waitFor({ state: 'hidden' });
await this.verifyToast({ message: `updated successfully.` });
await this.rootPage.locator('[data-nc="grid-load-spinner"]').waitFor({ state: 'hidden' });
await this.rootPage.locator('[data-testid="grid-load-spinner"]').waitFor({ state: 'hidden' });
}
async verify({ header, url }: { header: string; url: string }) {
@ -80,6 +86,7 @@ export class ExpandedFormPage extends BasePage {
async close() {
await this.rootPage.keyboard.press('Escape');
await this.get().waitFor({ state: 'hidden' });
}
async cancel() {
@ -87,7 +94,7 @@ export class ExpandedFormPage extends BasePage {
}
async openChildCard(param: { column: string; title: string }) {
const childList = await this.get().locator(`[data-nc="nc-expand-col-${param.column}"]`);
const childList = await this.get().locator(`[data-testid="nc-expand-col-${param.column}"]`);
await childList.locator(`.ant-card:has-text("${param.title}")`).click();
}

70
packages/nc-gui/tests/playwright/pages/Dashboard/Form/index.ts → tests/playwright/pages/Dashboard/Form/index.ts

@ -24,61 +24,65 @@ export class FormPage extends BasePage {
this.dashboard = dashboard;
this.toolbar = new ToolbarPage(this);
this.addAllButton = dashboard.get().locator('[data-nc="nc-form-add-all"]');
this.removeAllButton = dashboard.get().locator('[data-nc="nc-form-remove-all"]');
this.submitButton = dashboard.get().locator('[data-nc="nc-form-submit"]');
this.addAllButton = dashboard.get().locator('[data-testid="nc-form-add-all"]');
this.removeAllButton = dashboard.get().locator('[data-testid="nc-form-remove-all"]');
this.submitButton = dashboard.get().locator('[data-testid="nc-form-submit"]');
this.showAnotherFormRadioButton = dashboard.get().locator('[data-nc="nc-form-checkbox-submit-another-form"]');
this.showAnotherFormAfter5SecRadioButton = dashboard.get().locator('[data-nc="nc-form-checkbox-show-blank-form"]');
this.emailMeRadioButton = dashboard.get().locator('[data-nc="nc-form-checkbox-send-email"]');
this.formHeading = dashboard.get().locator('[data-nc="nc-form-heading"]');
this.formSubHeading = dashboard.get().locator('[data-nc="nc-form-sub-heading"]');
this.afterSubmitMsg = dashboard.get().locator('[data-nc="nc-form-after-submit-msg"]');
this.showAnotherFormRadioButton = dashboard.get().locator('[data-testid="nc-form-checkbox-submit-another-form"]');
this.showAnotherFormAfter5SecRadioButton = dashboard
.get()
.locator('[data-testid="nc-form-checkbox-show-blank-form"]');
this.emailMeRadioButton = dashboard.get().locator('[data-testid="nc-form-checkbox-send-email"]');
this.formHeading = dashboard.get().locator('[data-testid="nc-form-heading"]');
this.formSubHeading = dashboard.get().locator('[data-testid="nc-form-sub-heading"]');
this.afterSubmitMsg = dashboard.get().locator('[data-testid="nc-form-after-submit-msg"]');
}
get() {
return this.dashboard.get().locator('[data-nc="nc-form-wrapper"]');
return this.dashboard.get().locator('[data-testid="nc-form-wrapper"]');
}
getFormAfterSubmit() {
return this.dashboard.get().locator('[data-nc="nc-form-wrapper-submit"]');
return this.dashboard.get().locator('[data-testid="nc-form-wrapper-submit"]');
}
getFormHiddenColumn() {
return this.get().locator('[data-nc="nc-form-hidden-column"]');
return this.get().locator('[data-testid="nc-form-hidden-column"]');
}
getFormFields() {
return this.get().locator('[data-nc="nc-form-fields"]');
return this.get().locator('[data-testid="nc-form-fields"]');
}
getDragNDropToHide() {
return this.get().locator('[data-nc="nc-drag-n-drop-to-hide"]');
return this.get().locator('[data-testid="nc-drag-n-drop-to-hide"]');
}
getFormFieldsRemoveIcon() {
return this.get().locator('[data-nc="nc-field-remove-icon"]');
return this.get().locator('[data-testid="nc-field-remove-icon"]');
}
getFormFieldsRequired() {
return this.get().locator('[data-nc="nc-form-input-required"]');
return this.get().locator('[data-testid="nc-form-input-required"]');
}
getFormFieldsInputLabel() {
return this.get().locator('input[data-nc="nc-form-input-label"]:visible');
return this.get().locator('input[data-testid="nc-form-input-label"]:visible');
}
getFormFieldsInputHelpText() {
return this.get().locator('input[data-nc="nc-form-input-help-text"]:visible');
return this.get().locator('input[data-testid="nc-form-input-help-text"]:visible');
}
async verifyFormFieldLabel({ index, label }: { index: number; label: string }) {
await expect(await this.getFormFields().nth(index).locator('[data-nc="nc-form-input-label"]')).toContainText(label);
await expect(await this.getFormFields().nth(index).locator('[data-testid="nc-form-input-label"]')).toContainText(
label
);
}
async verifyFormFieldHelpText({ index, helpText }: { index: number; helpText: string }) {
await expect(
await this.getFormFields().nth(index).locator('[data-nc="nc-form-input-help-text-label"]')
await this.getFormFields().nth(index).locator('[data-testid="nc-form-input-help-text-label"]')
).toContainText(helpText);
}
@ -91,7 +95,7 @@ export class FormPage extends BasePage {
}
async verifyFormViewFieldsOrder({ fields }: { fields: string[] }) {
const fieldLabels = await this.get().locator('[data-nc="nc-form-input-label"]');
const fieldLabels = await this.get().locator('[data-testid="nc-form-input-label"]');
await expect(await fieldLabels).toHaveCount(fields.length);
for (let i = 0; i < fields.length; i++) {
await expect(await fieldLabels.nth(i)).toContainText(fields[i]);
@ -109,21 +113,21 @@ export class FormPage extends BasePage {
async removeField({ field, mode }: { mode: string; field: string }) {
if (mode === 'dragDrop') {
const src = await this.get().locator(`.nc-form-drag-${field.replace(' ', '')}`);
const dst = await this.get().locator(`[data-nc="nc-drag-n-drop-to-hide"]`);
const dst = await this.get().locator(`[data-testid="nc-drag-n-drop-to-hide"]`);
await src.dragTo(dst);
} else if (mode === 'hideField') {
const src = await this.get().locator(`.nc-form-drag-${field.replace(' ', '')}`);
await src.locator(`[data-nc="nc-field-remove-icon"]`).click();
await src.locator(`[data-testid="nc-field-remove-icon"]`).click();
}
}
async addField({ field, mode }: { mode: string; field: string }) {
if (mode === 'dragDrop') {
const src = await this.get().locator(`[data-nc="nc-form-hidden-column-${field}"]`);
const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"]`);
const dst = await this.get().locator(`.nc-form-drag-Country`);
await src.dragTo(dst);
} else if (mode === 'clickField') {
const src = await this.get().locator(`[data-nc="nc-form-hidden-column-${field}"]`);
const src = await this.get().locator(`[data-testid="nc-form-hidden-column-${field}"]`);
await src.click();
}
}
@ -149,7 +153,7 @@ export class FormPage extends BasePage {
async fillForm(param: { field: string; value: string }[]) {
for (let i = 0; i < param.length; i++) {
await this.get()
.locator(`[data-nc="nc-form-input-${param[i].field.replace(' ', '')}"] >> input`)
.locator(`[data-testid="nc-form-input-${param[i].field.replace(' ', '')}"] >> input`)
.fill(param[i].value);
}
}
@ -167,7 +171,7 @@ export class FormPage extends BasePage {
}) {
await this.get()
.locator(`.nc-form-drag-${field.replace(' ', '')}`)
.locator('div[data-nc="nc-form-input-label"]')
.locator('div[data-testid="nc-form-input-label"]')
.click();
await this.getFormFieldsInputLabel().fill(label);
await this.getFormFieldsInputHelpText().fill(helpText);
@ -196,12 +200,12 @@ export class FormPage extends BasePage {
const fieldLabel = await this.get()
.locator(`.nc-form-drag-${field.replace(' ', '')}`)
.locator('div[data-nc="nc-form-input-label"]');
.locator('div[data-testid="nc-form-input-label"]');
await expect(fieldLabel).toHaveText(expectText);
const fieldHelpText = await this.get()
.locator(`.nc-form-drag-${field.replace(' ', '')}`)
.locator('div[data-nc="nc-form-input-help-text-label"]');
.locator('div[data-testid="nc-form-input-help-text-label"]');
await expect(fieldHelpText).toHaveText(helpText);
}
@ -237,16 +241,18 @@ export class FormPage extends BasePage {
async verifyAfterSubmitMenuState(param: { showBlankForm?: boolean; submitAnotherForm?: boolean; emailMe?: boolean }) {
if (true === param.showBlankForm) {
await expect(
this.get().locator('[data-nc="nc-form-checkbox-show-blank-form"][aria-checked="true"]')
this.get().locator('[data-testid="nc-form-checkbox-show-blank-form"][aria-checked="true"]')
).toBeVisible();
}
if (true === param.submitAnotherForm) {
await expect(
this.get().locator('[data-nc="nc-form-checkbox-submit-another-form"][aria-checked="true"]')
this.get().locator('[data-testid="nc-form-checkbox-submit-another-form"][aria-checked="true"]')
).toBeVisible();
}
if (true === param.emailMe) {
await expect(this.get().locator('[data-nc="nc-form-checkbox-send-email"][aria-checked="true"]')).toBeVisible();
await expect(
this.get().locator('[data-testid="nc-form-checkbox-send-email"][aria-checked="true"]')
).toBeVisible();
}
}
}

2
packages/nc-gui/tests/playwright/pages/Dashboard/Gallery/index.ts → tests/playwright/pages/Dashboard/Gallery/index.ts

@ -13,7 +13,7 @@ export class GalleryPage extends BasePage {
}
get() {
return this.dashboard.get().locator('[data-nc="nc-gallery-wrapper"]');
return this.dashboard.get().locator('[data-testid="nc-gallery-wrapper"]');
}
card(index: number) {

10
packages/nc-gui/tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts → tests/playwright/pages/Dashboard/Grid/Column/LTAR/ChildList.ts

@ -21,7 +21,7 @@ export class ChildList extends BasePage {
// icon: reload
await expect(this.get().locator(`.ant-modal-title`)).toHaveText(`Child list`);
await expect(await this.get().locator(`button:has-text("Link to '${linkField}'")`).isVisible()).toBeTruthy();
await expect(await this.get().locator(`[data-cy="nc-child-list-reload"]`).isVisible()).toBeTruthy();
await expect(await this.get().locator(`[data-testid="nc-child-list-reload"]`).isVisible()).toBeTruthy();
// child list body validation (card count, card title)
const cardCount = cardTitle.length;
@ -34,8 +34,12 @@ export class ChildList extends BasePage {
await expect(await childList.nth(i).textContent()).toContain(cardTitle[i]);
// icon: unlink
// icon: delete
await expect(await childList.nth(i).locator(`[data-cy="nc-child-list-icon-unlink"]`).isVisible()).toBeTruthy();
await expect(await childList.nth(i).locator(`[data-cy="nc-child-list-icon-delete"]`).isVisible()).toBeTruthy();
await expect(
await childList.nth(i).locator(`[data-testid="nc-child-list-icon-unlink"]`).isVisible()
).toBeTruthy();
await expect(
await childList.nth(i).locator(`[data-testid="nc-child-list-icon-delete"]`).isVisible()
).toBeTruthy();
}
}
}

0
packages/nc-gui/tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts → tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts

14
packages/nc-gui/tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts → tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts

@ -29,8 +29,8 @@ export class SelectOptionColumnPageObject extends BasePage {
await this.column.get().locator('button:has-text("Add option")').click();
// Fill text=Select options can't be nullAdd option >> input[type="text"]
await this.column.get().locator(`[data-nc="select-column-option-input-${index}"]`).click();
await this.column.get().locator(`[data-nc="select-column-option-input-${index}"]`).fill(option);
await this.column.get().locator(`[data-testid="select-column-option-input-${index}"]`).click();
await this.column.get().locator(`[data-testid="select-column-option-input-${index}"]`).fill(option);
if (!skipColumnModal && columnTitle) await this.column.save({ isUpdated: true });
}
@ -38,8 +38,8 @@ export class SelectOptionColumnPageObject extends BasePage {
async editOption({ columnTitle, index, newOption }: { index: number; columnTitle: string; newOption: string }) {
await this.column.openEdit({ title: columnTitle });
await this.column.get().locator(`[data-nc="select-column-option-input-${index}"]`).click();
await this.column.get().locator(`[data-nc="select-column-option-input-${index}"]`).fill(newOption);
await this.column.get().locator(`[data-testid="select-column-option-input-${index}"]`).click();
await this.column.get().locator(`[data-testid="select-column-option-input-${index}"]`).fill(newOption);
await this.column.save({ isUpdated: true });
}
@ -47,7 +47,7 @@ export class SelectOptionColumnPageObject extends BasePage {
async deleteOption({ columnTitle, index }: { index: number; columnTitle: string }) {
await this.column.openEdit({ title: columnTitle });
await this.column.get().locator(`svg[data-nc="select-column-option-remove-${index}"]`).click();
await this.column.get().locator(`svg[data-testid="select-column-option-remove-${index}"]`).click();
await this.column.save({ isUpdated: true });
}
@ -66,8 +66,8 @@ export class SelectOptionColumnPageObject extends BasePage {
await this.column.rootPage.waitForTimeout(150);
await this.column.rootPage.dragAndDrop(
`svg[data-nc="select-option-column-handle-icon-${sourceOption}"]`,
`svg[data-nc="select-option-column-handle-icon-${destinationOption}"]`,
`svg[data-testid="select-option-column-handle-icon-${sourceOption}"]`,
`svg[data-testid="select-option-column-handle-icon-${destinationOption}"]`,
{
force: true,
}

2
packages/nc-gui/tests/playwright/pages/Dashboard/Grid/Column/index.ts → tests/playwright/pages/Dashboard/Grid/Column/index.ts

@ -14,7 +14,7 @@ export class ColumnPageObject extends BasePage {
}
get() {
return this.rootPage.locator('form[data-nc="add-or-edit-column"]');
return this.rootPage.locator('form[data-testid="add-or-edit-column"]');
}
async create({

38
packages/nc-gui/tests/playwright/pages/Dashboard/Grid/index.ts → tests/playwright/pages/Dashboard/Grid/index.ts

@ -26,11 +26,11 @@ export class GridPage extends BasePage {
}
get() {
return this.dashboard.get().locator('[data-nc="nc-grid-wrapper"]');
return this.dashboard.get().locator('[data-testid="nc-grid-wrapper"]');
}
row(index: number) {
return this.get().locator(`tr[data-nc="grid-row-${index}"]`);
return this.get().locator(`tr[data-testid="grid-row-${index}"]`);
}
async rowCount() {
@ -80,7 +80,7 @@ export class GridPage extends BasePage {
uiAction: clickOnColumnHeaderToSave,
requestUrlPathToMatch: 'api/v1/db/data/noco',
httpMethodsToMatch: ['POST'],
responseJsonMatcher: resJson => resJson?.[columnHeader] === value,
responseJsonMatcher: resJson => resJson?.[columnHeader] === rowValue,
});
} else {
await this.rootPage.waitForTimeout(300);
@ -122,17 +122,17 @@ export class GridPage extends BasePage {
}
async verifyRow({ index }: { index: number }) {
await this.get().locator(`td[data-nc="cell-Title-${index}"]`).waitFor({ state: 'visible' });
await expect(this.get().locator(`td[data-nc="cell-Title-${index}"]`)).toHaveCount(1);
await this.get().locator(`td[data-testid="cell-Title-${index}"]`).waitFor({ state: 'visible' });
await expect(this.get().locator(`td[data-testid="cell-Title-${index}"]`)).toHaveCount(1);
}
async verifyRowDoesNotExist({ index }: { index: number }) {
await this.get().locator(`td[data-nc="cell-Title-${index}"]`).waitFor({ state: 'hidden' });
return await expect(this.get().locator(`td[data-nc="cell-Title-${index}"]`)).toHaveCount(0);
await this.get().locator(`td[data-testid="cell-Title-${index}"]`).waitFor({ state: 'hidden' });
return await expect(this.get().locator(`td[data-testid="cell-Title-${index}"]`)).toHaveCount(0);
}
async deleteRow(index: number) {
await this.get().locator(`td[data-nc="cell-Title-${index}"]`).click({
await this.get().locator(`td[data-testid="cell-Title-${index}"]`).click({
button: 'right',
});
@ -149,7 +149,7 @@ export class GridPage extends BasePage {
async addRowRightClickMenu(index: number) {
const rowCount = await this.get().locator('.nc-grid-row').count();
await this.get().locator(`td[data-nc="cell-Title-${index}"]`).click({
await this.get().locator(`td[data-testid="cell-Title-${index}"]`).click({
button: 'right',
});
// Click text=Insert New Row
@ -158,28 +158,30 @@ export class GridPage extends BasePage {
}
async openExpandedRow({ index }: { index: number }) {
await this.row(index).locator(`td[data-nc="cell-Id-${index}"]`).hover();
await this.row(index).locator(`div[data-nc="nc-expand-${index}"]`).click();
await this.row(index).locator(`td[data-testid="cell-Id-${index}"]`).hover();
await this.row(index).locator(`div[data-testid="nc-expand-${index}"]`).click();
await (await this.rootPage.locator('.ant-drawer-body').elementHandle())?.waitForElementState('stable');
}
async selectAll() {
await this.get().locator('[data-nc="nc-check-all"]').hover();
await this.get().locator('[data-testid="nc-check-all"]').hover();
await this.get().locator('[data-nc="nc-check-all"]').locator('input[type="checkbox"]').check({
await this.get().locator('[data-testid="nc-check-all"]').locator('input[type="checkbox"]').check({
force: true,
});
const rowCount = await this.rowCount();
for (let i = 0; i < rowCount; i++) {
await expect(this.row(i).locator(`[data-nc="cell-Id-${i}"]`).locator('span.ant-checkbox-checked')).toHaveCount(1);
await expect(
this.row(i).locator(`[data-testid="cell-Id-${i}"]`).locator('span.ant-checkbox-checked')
).toHaveCount(1);
}
await this.rootPage.waitForTimeout(300);
}
async deleteAll() {
await this.selectAll();
await this.get().locator('[data-nc="nc-check-all"]').nth(0).click({
await this.get().locator('[data-testid="nc-check-all"]').nth(0).click({
button: 'right',
});
await this.rootPage.locator('text=Delete Selected Rows').click();
@ -211,7 +213,7 @@ export class GridPage extends BasePage {
}
async waitLoading() {
await this.dashboard.get().locator('[data-nc="grid-load-spinner"]').waitFor({ state: 'hidden' });
await this.dashboard.get().locator('[data-testid="grid-load-spinner"]').waitFor({ state: 'hidden' });
}
async verifyEditDisabled({ columnHeader = 'Title' }: { columnHeader?: string } = {}) {
@ -224,7 +226,7 @@ export class GridPage extends BasePage {
await expect(await cell.locator('input')).not.toBeVisible();
// right click menu
await this.get().locator(`td[data-nc="cell-${columnHeader}-0"]`).click({
await this.get().locator(`td[data-testid="cell-${columnHeader}-0"]`).click({
button: 'right',
});
await expect(await this.rootPage.locator('text=Insert New Row')).not.toBeVisible();
@ -252,7 +254,7 @@ export class GridPage extends BasePage {
await expect(await cell.locator('input')).toBeVisible();
// right click menu
await this.get().locator(`td[data-nc="cell-${columnHeader}-0"]`).click({
await this.get().locator(`td[data-testid="cell-${columnHeader}-0"]`).click({
button: 'right',
});
await expect(await this.rootPage.locator('text=Insert New Row')).toBeVisible();

0
packages/nc-gui/tests/playwright/pages/Dashboard/Import/Airtable.ts → tests/playwright/pages/Dashboard/Import/Airtable.ts

0
packages/nc-gui/tests/playwright/pages/Dashboard/Import/ImportTemplate.ts → tests/playwright/pages/Dashboard/Import/ImportTemplate.ts

4
packages/nc-gui/tests/playwright/pages/Dashboard/Kanban/index.ts → tests/playwright/pages/Dashboard/Kanban/index.ts

@ -14,7 +14,7 @@ export class KanbanPage extends BasePage {
}
get() {
return this.dashboard.get().locator('[data-nc="nc-kanban-wrapper"]');
return this.dashboard.get().locator('[data-testid="nc-kanban-wrapper"]');
}
card(index: number) {
@ -65,7 +65,7 @@ export class KanbanPage extends BasePage {
for (let i = 0; i < stacks; i++) {
const stack = await this.get().locator(`.nc-kanban-stack`).nth(i);
// Since otherwise stack title will be repeated as title is in two divs, with one having hidden class
const stackTitle = await stack.locator(`.nc-kanban-stack-head >> [data-nc="truncate-label"]`);
const stackTitle = await stack.locator(`.nc-kanban-stack-head >> [data-testid="truncate-label"]`);
await expect(stackTitle).toHaveText(order[i], { ignoreCase: true });
}
}

2
packages/nc-gui/tests/playwright/pages/Dashboard/Settings/Acl.ts → tests/playwright/pages/Dashboard/Settings/Acl.ts

@ -11,7 +11,7 @@ export class AclPage extends BasePage {
}
get() {
return this.settings.get().locator(`[data-nc="nc-settings-subtab-UI Access Control"]`);
return this.settings.get().locator(`[data-testid="nc-settings-subtab-UI Access Control"]`);
}
async toggle({ table, role }: { table: string; role: string }) {

2
packages/nc-gui/tests/playwright/pages/Dashboard/Settings/AppStore.ts → tests/playwright/pages/Dashboard/Settings/AppStore.ts

@ -11,7 +11,7 @@ export class AppStoreSettingsPage extends BasePage {
}
get() {
return this.settings.get().locator(`[data-nc="nc-settings-subtab-appStore"]`);
return this.settings.get().locator(`[data-testid="nc-settings-subtab-appStore"]`);
}
async install({ name }: { name: string }) {

2
packages/nc-gui/tests/playwright/pages/Dashboard/Settings/Audit.ts → tests/playwright/pages/Dashboard/Settings/Audit.ts

@ -11,7 +11,7 @@ export class AuditSettingsPage extends BasePage {
}
get() {
return this.settings.get().locator(`[data-nc="nc-settings-subtab-Audit"]`);
return this.settings.get().locator(`[data-testid="nc-settings-subtab-Audit"]`);
}
async verifyRow({

2
packages/nc-gui/tests/playwright/pages/Dashboard/Settings/Erd.ts → tests/playwright/pages/Dashboard/Settings/Erd.ts

@ -10,6 +10,6 @@ export class SettingsErdPage extends ErdBasePage {
}
get() {
return this.rootPage.locator(`[data-nc="nc-settings-subtab-ERD View"]`);
return this.rootPage.locator(`[data-testid="nc-settings-subtab-ERD View"]`);
}
}

2
packages/nc-gui/tests/playwright/pages/Dashboard/Settings/Metadata.ts → tests/playwright/pages/Dashboard/Settings/Metadata.ts

@ -11,7 +11,7 @@ export class MetaDataPage extends BasePage {
}
get() {
return this.settings.get().locator(`[data-nc="nc-settings-subtab-Metadata"]`);
return this.settings.get().locator(`[data-testid="nc-settings-subtab-Metadata"]`);
}
async clickReload() {

2
packages/nc-gui/tests/playwright/pages/Dashboard/Settings/Miscellaneous.ts → tests/playwright/pages/Dashboard/Settings/Miscellaneous.ts

@ -11,7 +11,7 @@ export class MiscSettingsPage extends BasePage {
}
get() {
return this.settings.get().locator(`[data-nc="nc-settings-subtab-Miscellaneous"]`);
return this.settings.get().locator(`[data-testid="nc-settings-subtab-Miscellaneous"]`);
}
async clickShowM2MTables() {

4
packages/nc-gui/tests/playwright/pages/Dashboard/Settings/Teams.ts → tests/playwright/pages/Dashboard/Settings/Teams.ts

@ -17,7 +17,7 @@ export class TeamsPage extends BasePage {
}
get() {
return this.settings.get().locator(`[data-nc="nc-settings-subtab-Users Management"]`);
return this.settings.get().locator(`[data-testid="nc-settings-subtab-Users Management"]`);
}
prefixEmail(email: string) {
@ -26,7 +26,7 @@ export class TeamsPage extends BasePage {
}
getSharedBaseSubModal() {
return this.rootPage.locator(`[data-nc="nc-share-base-sub-modal"]`);
return this.rootPage.locator(`[data-testid="nc-share-base-sub-modal"]`);
}
async invite({ email, role }: { email: string; role: string }) {

2
packages/nc-gui/tests/playwright/pages/Dashboard/Settings/index.ts → tests/playwright/pages/Dashboard/Settings/index.ts

@ -57,7 +57,7 @@ export class SettingsPage extends BasePage {
}
async close() {
await this.get().locator('[data-nc="settings-modal-close-button"]').click();
await this.get().locator('[data-testid="settings-modal-close-button"]').click();
await this.get().waitFor({ state: 'hidden' });
}
}

26
packages/nc-gui/tests/playwright/pages/Dashboard/SurveyForm/index.ts → tests/playwright/pages/Dashboard/SurveyForm/index.ts

@ -13,14 +13,14 @@ export class SurveyFormPage extends BasePage {
constructor(rootPage: Page) {
super(rootPage);
this.formHeading = this.get().locator('[data-nc="nc-survey-form__heading"]');
this.formSubHeading = this.get().locator('[data-nc="nc-survey-form__sub-heading"]');
this.submitButton = this.get().locator('[data-nc="nc-survey-form__btn-submit"]');
this.nextButton = this.get().locator('[data-nc="nc-survey-form__btn-next"]');
this.nextSlideButton = this.get().locator('[data-nc="nc-survey-form__icon-next"]');
this.prevSlideButton = this.get().locator('[data-nc="nc-survey-form__icon-prev"]');
this.darkModeButton = this.get().locator('[data-nc="nc-form-dark-mode"]');
this.formFooter = this.get().locator('[data-nc="nc-survey-form__footer"]');
this.formHeading = this.get().locator('[data-testid="nc-survey-form__heading"]');
this.formSubHeading = this.get().locator('[data-testid="nc-survey-form__sub-heading"]');
this.submitButton = this.get().locator('[data-testid="nc-survey-form__btn-submit"]');
this.nextButton = this.get().locator('[data-testid="nc-survey-form__btn-next"]');
this.nextSlideButton = this.get().locator('[data-testid="nc-survey-form__icon-next"]');
this.prevSlideButton = this.get().locator('[data-testid="nc-survey-form__icon-prev"]');
this.darkModeButton = this.get().locator('[data-testid="nc-form-dark-mode"]');
this.formFooter = this.get().locator('[data-testid="nc-survey-form__footer"]');
}
get() {
@ -42,7 +42,7 @@ export class SurveyFormPage extends BasePage {
await expect(this.formHeading).toHaveText(heading);
await expect(this.formSubHeading).toHaveText(subHeading);
await expect(this.formFooter).toHaveText(footer);
await expect(this.get().locator(`[data-nc="nc-form-column-label"]`)).toHaveText(fieldLabel);
await expect(this.get().locator(`[data-testid="nc-form-column-label"]`)).toHaveText(fieldLabel);
// parse footer text ("1 / 3") to identify if last slide
let isLastSlide = false;
@ -60,11 +60,11 @@ export class SurveyFormPage extends BasePage {
}
async fill(param: { fieldLabel: string; type?: string; value?: string }) {
await this.get().locator(`[data-nc="nc-survey-form__input-${param.fieldLabel}"]`).click();
await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"]`).click();
if (param.type === 'SingleLineText') {
await this.get().locator(`[data-nc="nc-survey-form__input-${param.fieldLabel}"] >> input`).fill(param.value);
await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"] >> input`).fill(param.value);
// press enter key
await this.get().locator(`[data-nc="nc-survey-form__input-${param.fieldLabel}"] >> input`).press('Enter');
await this.get().locator(`[data-testid="nc-survey-form__input-${param.fieldLabel}"] >> input`).press('Enter');
} else if (param.type === 'DateTime') {
const modal = await this.rootPage.locator('.nc-picker-datetime');
await expect(modal).toBeVisible();
@ -76,7 +76,7 @@ export class SurveyFormPage extends BasePage {
async validateSuccessMessage(param: { message: string; showAnotherForm?: boolean }) {
await expect(
this.get().locator(`[data-nc="nc-survey-form__success-msg"]:has-text("${param.message}")`)
this.get().locator(`[data-testid="nc-survey-form__success-msg"]:has-text("${param.message}")`)
).toBeVisible();
if (param.showAnotherForm) {

6
packages/nc-gui/tests/playwright/pages/Dashboard/TreeView.ts → tests/playwright/pages/Dashboard/TreeView.ts

@ -48,7 +48,7 @@ export class TreeViewPage extends BasePage {
await this.dashboard.get().locator('.nc-modal-table-create').locator('.ant-modal-body').waitFor();
await this.dashboard.get().locator('[placeholder="Enter table name"]').fill(title);
await this.dashboard.get().getByPlaceholder('Enter table name').fill(title);
await this.waitForResponse({
uiAction: this.dashboard.get().locator('button:has-text("Submit")').click(),
@ -107,8 +107,8 @@ export class TreeViewPage extends BasePage {
async reorderTables({ sourceTable, destinationTable }: { sourceTable: string; destinationTable: string }) {
await this.dashboard
.get()
.locator(`[data-nc="tree-view-table-draggable-handle-${sourceTable}"]`)
.dragTo(this.get().locator(`[data-nc="tree-view-table-${destinationTable}"]`));
.locator(`[data-testid="tree-view-table-draggable-handle-${sourceTable}"]`)
.dragTo(this.get().locator(`[data-testid="tree-view-table-${destinationTable}"]`));
}
async quickImport({ title }: { title: string }) {

28
packages/nc-gui/tests/playwright/pages/Dashboard/ViewSidebar/index.ts → tests/playwright/pages/Dashboard/ViewSidebar/index.ts

@ -1,5 +1,5 @@
import { expect, Locator } from '@playwright/test';
import { DashboardPage } from '../';
import { DashboardPage } from '..';
import BasePage from '../../Base';
export class ViewSidebarPage extends BasePage {
@ -54,7 +54,7 @@ export class ViewSidebarPage extends BasePage {
}
async openView({ title }: { title: string }) {
await this.get().locator(`[data-nc="view-sidebar-view-${title}"]`).click();
await this.get().locator(`[data-testid="view-sidebar-view-${title}"]`).click();
}
async createKanbanView({ title }: { title: string }) {
@ -64,7 +64,7 @@ export class ViewSidebarPage extends BasePage {
// Todo: Make selection better
async verifyView({ title, index }: { title: string; index: number }) {
await expect(
this.get().locator('[data-nc="view-item"]').nth(index).locator('[data-nc="truncate-label"]')
this.get().locator('[data-testid="view-item"]').nth(index).locator('[data-testid="truncate-label"]')
).toHaveText(title, { ignoreCase: true });
}
@ -82,13 +82,16 @@ export class ViewSidebarPage extends BasePage {
async reorderViews({ sourceView, destinationView }: { sourceView: string; destinationView: string }) {
await this.dashboard
.get()
.locator(`[data-nc="view-sidebar-drag-handle-${sourceView}"]`)
.dragTo(this.get().locator(`[data-nc="view-sidebar-view-${destinationView}"]`));
.locator(`[data-testid="view-sidebar-drag-handle-${sourceView}"]`)
.dragTo(this.get().locator(`[data-testid="view-sidebar-view-${destinationView}"]`));
}
async deleteView({ title }: { title: string }) {
await this.get().locator(`[data-nc="view-sidebar-view-${title}"]`).hover();
await this.get().locator(`[data-nc="view-sidebar-view-actions-${title}"]`).locator('.nc-view-delete-icon').click();
await this.get().locator(`[data-testid="view-sidebar-view-${title}"]`).hover();
await this.get()
.locator(`[data-testid="view-sidebar-view-actions-${title}"]`)
.locator('.nc-view-delete-icon')
.click();
await this.rootPage.locator('.nc-modal-view-delete').locator('button:has-text("Submit"):visible').click();
@ -101,15 +104,18 @@ export class ViewSidebarPage extends BasePage {
}
async renameView({ title, newTitle }: { title: string; newTitle: string }) {
await this.get().locator(`[data-nc="view-sidebar-view-${title}"]`).dblclick();
await this.get().locator(`[data-nc="view-sidebar-view-${title}"]`).locator('input').fill(newTitle);
await this.get().locator(`[data-testid="view-sidebar-view-${title}"]`).dblclick();
await this.get().locator(`[data-testid="view-sidebar-view-${title}"]`).locator('input').fill(newTitle);
await this.get().press('Enter');
await this.verifyToast({ message: 'View renamed successfully' });
}
async copyView({ title }: { title: string }) {
await this.get().locator(`[data-nc="view-sidebar-view-${title}"]`).hover();
await this.get().locator(`[data-nc="view-sidebar-view-actions-${title}"]`).locator('.nc-view-copy-icon').click();
await this.get().locator(`[data-testid="view-sidebar-view-${title}"]`).hover();
await this.get()
.locator(`[data-testid="view-sidebar-view-actions-${title}"]`)
.locator('.nc-view-copy-icon')
.click();
const submitAction = this.rootPage
.locator('.ant-modal-content')
.locator('button:has-text("Submit"):visible')

0
packages/nc-gui/tests/playwright/pages/Dashboard/WebhookForm/index.ts → tests/playwright/pages/Dashboard/WebhookForm/index.ts

4
packages/nc-gui/tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts → tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts

@ -15,12 +15,12 @@ export class AttachmentCellPageObject extends BasePage {
}
clickFilePicker({ index, columnHeader }: { index?: number; columnHeader: string }) {
return this.get({ index, columnHeader }).locator('[data-nc="attachment-cell-file-picker-button"]').click();
return this.get({ index, columnHeader }).locator('[data-testid="attachment-cell-file-picker-button"]').click();
}
async addFile({ index, columnHeader, filePath }: { index?: number; columnHeader: string; filePath: string }) {
const attachFileAction = this.get({ index, columnHeader })
.locator('[data-nc="attachment-cell-file-picker-button"]')
.locator('[data-testid="attachment-cell-file-picker-button"]')
.click();
return await this.attachFile({ filePickUIAction: attachFileAction, filePath });
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save