mirror of https://github.com/nocodb/nocodb
Muhammed Mustafa
2 years ago
committed by
GitHub
167 changed files with 298854 additions and 56 deletions
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh |
||||
. "$(dirname -- "$0")/_/husky.sh" |
||||
|
||||
npx lint-staged |
@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager"> |
||||
<configuration default="false" name="test-debug" type="js.build_tools.npm"> |
||||
<package-json value="$PROJECT_DIR$/scripts/playwright/package.json" /> |
||||
<command value="run" /> |
||||
<scripts> |
||||
<script value="test-debug" /> |
||||
</scripts> |
||||
<node-interpreter value="project" /> |
||||
<envs /> |
||||
<method v="2" /> |
||||
</configuration> |
||||
</component> |
@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager"> |
||||
<configuration default="false" name="test" type="js.build_tools.npm" nameIsGenerated="true"> |
||||
<package-json value="$PROJECT_DIR$/scripts/playwright/package.json" /> |
||||
<command value="run" /> |
||||
<scripts> |
||||
<script value="test" /> |
||||
</scripts> |
||||
<node-interpreter value="project" /> |
||||
<envs /> |
||||
<method v="2" /> |
||||
</configuration> |
||||
</component> |
@ -0,0 +1,36 @@
|
||||
import Noco from '../../Noco'; |
||||
|
||||
import { Request, Router } from 'express'; |
||||
import { TestResetService } from '../../services/test/TestResetService'; |
||||
|
||||
export async function reset(req: Request<any, any>, res) { |
||||
const service = new TestResetService({ |
||||
parallelId: req.body.parallelId, |
||||
dbType: req.body.dbType, |
||||
isEmptyProject: req.body.isEmptyProject, |
||||
}); |
||||
|
||||
res.json(await service.process()); |
||||
} |
||||
|
||||
export async function sqliteExec(req: Request<any, any>, res) { |
||||
const metaKnex = Noco.ncMeta.knex; |
||||
try { |
||||
const result = await metaKnex.raw(req.body.sql); |
||||
res.json({ |
||||
body: result, |
||||
}); |
||||
} catch (e) { |
||||
console.error('sqliteExec', e); |
||||
res.status(500).json({ |
||||
error: e, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
const router = Router(); |
||||
|
||||
router.post('/api/v1/meta/test/reset', reset); |
||||
router.post('/api/v1/meta/test/sqlite_exec', sqliteExec); |
||||
|
||||
export default router; |
@ -0,0 +1,174 @@
|
||||
import Noco from '../../../Noco'; |
||||
|
||||
import Knex from 'knex'; |
||||
import axios from 'axios'; |
||||
import Project from '../../../models/Project'; |
||||
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; |
||||
import resetMetaSakilaSqliteProject from './resetMetaSakilaSqliteProject'; |
||||
import resetMysqlSakilaProject from './resetMysqlSakilaProject'; |
||||
import Model from '../../../models/Model'; |
||||
import resetPgSakilaProject from './resetPgSakilaProject'; |
||||
import User from '../../../models/User'; |
||||
import NocoCache from '../../../cache/NocoCache'; |
||||
import { CacheScope } from '../../../utils/globals'; |
||||
import ProjectUser from '../../../models/ProjectUser'; |
||||
|
||||
const loginRootUser = async () => { |
||||
const response = await axios.post( |
||||
'http://localhost:8080/api/v1/auth/user/signin', |
||||
{ email: 'user@nocodb.com', password: 'Password123.' } |
||||
); |
||||
|
||||
return response.data.token; |
||||
}; |
||||
|
||||
const projectTitleByType = { |
||||
sqlite: 'sampleREST', |
||||
mysql: 'externalREST', |
||||
pg: 'pgExtREST', |
||||
}; |
||||
|
||||
export class TestResetService { |
||||
private knex: Knex | null = null; |
||||
private readonly parallelId; |
||||
private readonly dbType; |
||||
private readonly isEmptyProject: boolean; |
||||
|
||||
constructor({ |
||||
parallelId, |
||||
dbType, |
||||
isEmptyProject, |
||||
}: { |
||||
parallelId: string; |
||||
dbType: string; |
||||
isEmptyProject: boolean; |
||||
}) { |
||||
this.knex = Noco.ncMeta.knex; |
||||
this.parallelId = parallelId; |
||||
this.dbType = dbType; |
||||
this.isEmptyProject = isEmptyProject; |
||||
} |
||||
|
||||
async process() { |
||||
try { |
||||
const token = await loginRootUser(); |
||||
|
||||
const { project } = await this.resetProject({ |
||||
metaKnex: this.knex, |
||||
token, |
||||
dbType: this.dbType, |
||||
parallelId: this.parallelId, |
||||
}); |
||||
|
||||
await removeAllPrefixedUsersExceptSuper(this.parallelId); |
||||
|
||||
return { token, project }; |
||||
} catch (e) { |
||||
console.error('TestResetService:process', e); |
||||
return { error: e }; |
||||
} |
||||
} |
||||
|
||||
async resetProject({ |
||||
metaKnex, |
||||
token, |
||||
dbType, |
||||
parallelId, |
||||
}: { |
||||
metaKnex: Knex; |
||||
token: string; |
||||
dbType: string; |
||||
parallelId: string; |
||||
}) { |
||||
const title = `${projectTitleByType[dbType]}${parallelId}`; |
||||
const project: Project | undefined = await Project.getByTitle(title); |
||||
|
||||
if (project) { |
||||
await removeProjectUsersFromCache(project); |
||||
|
||||
const bases = await project.getBases(); |
||||
if (dbType == 'sqlite') await dropTablesOfProject(metaKnex, project); |
||||
await Project.delete(project.id); |
||||
|
||||
if (bases.length > 0) await NcConnectionMgrv2.deleteAwait(bases[0]); |
||||
} |
||||
|
||||
if (dbType == 'sqlite') { |
||||
await resetMetaSakilaSqliteProject({ |
||||
token, |
||||
metaKnex, |
||||
title, |
||||
oldProject: project, |
||||
isEmptyProject: this.isEmptyProject, |
||||
}); |
||||
} else if (dbType == 'mysql') { |
||||
await resetMysqlSakilaProject({ |
||||
token, |
||||
title, |
||||
parallelId, |
||||
oldProject: project, |
||||
isEmptyProject: this.isEmptyProject, |
||||
}); |
||||
} else if (dbType == 'pg') { |
||||
await resetPgSakilaProject({ |
||||
token, |
||||
title, |
||||
parallelId, |
||||
oldProject: project, |
||||
isEmptyProject: this.isEmptyProject, |
||||
}); |
||||
} |
||||
|
||||
return { |
||||
project: await Project.getByTitle(title), |
||||
}; |
||||
} |
||||
} |
||||
|
||||
const dropTablesOfProject = async (knex: Knex, project: Project) => { |
||||
const tables = await Model.list({ |
||||
project_id: project.id, |
||||
base_id: (await project.getBases())[0].id, |
||||
}); |
||||
|
||||
for (const table of tables) { |
||||
if (table.type == 'table') { |
||||
await knex.raw(`DROP TABLE IF EXISTS ${table.table_name}`); |
||||
} else { |
||||
await knex.raw(`DROP VIEW IF EXISTS ${table.table_name}`); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
const removeAllPrefixedUsersExceptSuper = async (parallelId: string) => { |
||||
const users = (await User.list()).filter( |
||||
(user) => !user.roles.includes('super') |
||||
); |
||||
|
||||
for (const user of users) { |
||||
if(user.email.startsWith(`nc_test_${parallelId}_`)) { |
||||
await NocoCache.del(`${CacheScope.USER}:${user.email}`); |
||||
await User.delete(user.id); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// todo: Remove this once user deletion improvement PR is merged
|
||||
const removeProjectUsersFromCache = async (project: Project) => { |
||||
const projectUsers: ProjectUser[] = await ProjectUser.getUsersList({ |
||||
project_id: project.id, |
||||
limit: 1000, |
||||
offset: 0, |
||||
}); |
||||
|
||||
for (const projectUser of projectUsers) { |
||||
try { |
||||
const user: User = await User.get(projectUser.fk_user_id); |
||||
await NocoCache.del( |
||||
`${CacheScope.PROJECT_USER}:${project.id}:${user.id}` |
||||
); |
||||
} catch (e) { |
||||
console.error('removeProjectUsersFromCache', e); |
||||
} |
||||
} |
||||
}; |
@ -0,0 +1,151 @@
|
||||
import axios from 'axios'; |
||||
import Knex from 'knex'; |
||||
|
||||
import { promises as fs } from 'fs'; |
||||
import { sakilaTableNames } from '../../../utils/globals'; |
||||
import Project from '../../../models/Project'; |
||||
|
||||
const sqliteSakilaSqlViews = [ |
||||
'actor_info', |
||||
'customer_list', |
||||
'film_list', |
||||
'nice_but_slower_film_list', |
||||
'sales_by_film_category', |
||||
'sales_by_store', |
||||
'staff_list', |
||||
]; |
||||
|
||||
const resetMetaSakilaSqliteProject = async ({ |
||||
metaKnex, |
||||
token, |
||||
title, |
||||
oldProject, |
||||
isEmptyProject, |
||||
}: { |
||||
metaKnex: Knex; |
||||
token: string; |
||||
title: string; |
||||
oldProject: Project; |
||||
isEmptyProject: boolean; |
||||
}) => { |
||||
const project = await createProject(token, title); |
||||
|
||||
if (oldProject) await dropTablesAndViews(metaKnex, oldProject.prefix); |
||||
await dropTablesAndViews(metaKnex, project.prefix); |
||||
|
||||
if (isEmptyProject) return; |
||||
|
||||
await resetMetaSakilaSqlite(metaKnex, project.prefix, oldProject); |
||||
|
||||
await syncMeta(project, token); |
||||
}; |
||||
|
||||
const createProject = async (token: string, title: string) => { |
||||
const response = await axios.post( |
||||
'http://localhost:8080/api/v1/db/meta/projects/', |
||||
{ title }, |
||||
{ |
||||
headers: { |
||||
'xc-auth': token, |
||||
}, |
||||
} |
||||
); |
||||
if (response.status !== 200) { |
||||
console.error('Error creating project', response.data); |
||||
} |
||||
return response.data; |
||||
}; |
||||
|
||||
const syncMeta = async (project: Project, token: string) => { |
||||
await axios.post( |
||||
`http://localhost:8080/api/v1/db/meta/projects/${project.id}/meta-diff`, |
||||
{}, |
||||
{ |
||||
headers: { |
||||
'xc-auth': token, |
||||
}, |
||||
} |
||||
); |
||||
}; |
||||
|
||||
const dropTablesAndViews = async (metaKnex: Knex, prefix: string) => { |
||||
try { |
||||
for (const view of sqliteSakilaSqlViews) { |
||||
await metaKnex.raw(`DROP VIEW IF EXISTS ${prefix}${view}`); |
||||
} |
||||
|
||||
for (const table of sakilaTableNames) { |
||||
await metaKnex.raw(`DROP TABLE IF EXISTS ${prefix}${table}`); |
||||
} |
||||
} catch (e) { |
||||
console.error('Error dropping tables and views', e); |
||||
} |
||||
}; |
||||
|
||||
const resetMetaSakilaSqlite = async ( |
||||
metaKnex: Knex, |
||||
prefix: string, |
||||
oldProject: Project |
||||
) => { |
||||
await dropTablesAndViews(metaKnex, oldProject.prefix); |
||||
|
||||
const testsDir = __dirname.replace( |
||||
'/src/lib/services/test/TestResetService', |
||||
'/tests' |
||||
); |
||||
|
||||
try { |
||||
const schemaFile = await fs.readFile( |
||||
`${testsDir}/sqlite-sakila-db/03-sqlite-prefix-sakila-schema.sql` |
||||
); |
||||
const schemaFileStr = schemaFile.toString().replace(/prefix___/g, prefix); |
||||
|
||||
const schemaSqlQueries = schemaFileStr |
||||
.split(';') |
||||
.filter((str) => str.trim().length > 0) |
||||
.map((str) => str.trim()); |
||||
for (const sqlQuery of schemaSqlQueries) { |
||||
if (sqlQuery.trim().length > 0) { |
||||
await metaKnex.raw( |
||||
sqlQuery |
||||
.trim() |
||||
.replace(/WHERE rowid = new.rowid/g, '$&;') |
||||
); |
||||
} |
||||
} |
||||
} catch (e) { |
||||
console.error('Error resetting meta sakila sqlite:db', e); |
||||
} |
||||
|
||||
const dataFile = await fs.readFile( |
||||
`${testsDir}/sqlite-sakila-db/04-sqlite-prefix-sakila-insert-data.sql` |
||||
); |
||||
const dataFileStr = dataFile.toString().replace(/prefix___/g, prefix); |
||||
const dataSqlQueries = dataFileStr |
||||
.split(';') |
||||
.filter((str) => str.trim().length > 0) |
||||
.map((str) => str.trim()); |
||||
|
||||
const batchSize = 1000; |
||||
const batches = dataSqlQueries.reduce((acc, _, i) => { |
||||
if (!(i % batchSize)) { |
||||
// if index is 0 or can be divided by the `size`...
|
||||
acc.push(dataSqlQueries.slice(i, i + batchSize)); // ..push a chunk of the original array to the accumulator
|
||||
} |
||||
return acc; |
||||
}, []); |
||||
|
||||
for (const sqlQueryBatch of batches) { |
||||
const trx = await metaKnex.transaction(); |
||||
|
||||
for (const sqlQuery of sqlQueryBatch) { |
||||
await trx.raw(sqlQuery); |
||||
} |
||||
|
||||
await trx.commit(); |
||||
// wait for 40 ms to avoid SQLITE_BUSY error
|
||||
await new Promise((resolve) => setTimeout(resolve, 40)); |
||||
} |
||||
}; |
||||
|
||||
export default resetMetaSakilaSqliteProject; |
@ -0,0 +1,154 @@
|
||||
import axios from 'axios'; |
||||
import Knex from 'knex'; |
||||
|
||||
import { promises as fs } from 'fs'; |
||||
import Audit from '../../../models/Audit'; |
||||
import Project from '../../../models/Project'; |
||||
|
||||
const config = { |
||||
client: 'mysql2', |
||||
connection: { |
||||
host: 'localhost', |
||||
port: 3306, |
||||
user: 'root', |
||||
password: 'password', |
||||
database: 'sakila', |
||||
multipleStatements: true, |
||||
dateStrings: true, |
||||
}, |
||||
}; |
||||
|
||||
const extMysqlProject = (title, parallelId) => ({ |
||||
title, |
||||
bases: [ |
||||
{ |
||||
type: 'mysql2', |
||||
config: { |
||||
client: 'mysql2', |
||||
connection: { |
||||
host: 'localhost', |
||||
port: '3306', |
||||
user: 'root', |
||||
password: 'password', |
||||
database: `test_sakila_${parallelId}`, |
||||
}, |
||||
}, |
||||
inflection_column: 'camelize', |
||||
inflection_table: 'camelize', |
||||
}, |
||||
], |
||||
external: true, |
||||
}); |
||||
|
||||
const isSakilaMysqlToBeReset = async ( |
||||
knex: Knex, |
||||
parallelId: string, |
||||
project?: Project |
||||
) => { |
||||
const tablesInDb: Array<string> = await knex.raw( |
||||
`SELECT table_name FROM information_schema.tables WHERE table_schema = 'test_sakila_${parallelId}'` |
||||
); |
||||
|
||||
if ( |
||||
tablesInDb.length === 0 || |
||||
(tablesInDb.length > 0 && !tablesInDb.includes(`actor`)) |
||||
) { |
||||
return true; |
||||
} |
||||
|
||||
if (!project) return false; |
||||
|
||||
const audits = await Audit.projectAuditList(project.id, {}); |
||||
|
||||
return audits?.length > 0; |
||||
}; |
||||
|
||||
const resetSakilaMysql = async ( |
||||
knex: Knex, |
||||
parallelId: string, |
||||
isEmptyProject: boolean |
||||
) => { |
||||
const testsDir = __dirname.replace( |
||||
'/src/lib/services/test/TestResetService', |
||||
'/tests' |
||||
); |
||||
|
||||
try { |
||||
await knex.raw(`DROP DATABASE test_sakila_${parallelId}`); |
||||
} catch (e) { |
||||
console.log('Error dropping db', e); |
||||
} |
||||
await knex.raw(`CREATE DATABASE test_sakila_${parallelId}`); |
||||
|
||||
if (isEmptyProject) return; |
||||
|
||||
const trx = await knex.transaction(); |
||||
|
||||
try { |
||||
const schemaFile = await fs.readFile( |
||||
`${testsDir}/mysql-sakila-db/03-test-sakila-schema.sql` |
||||
); |
||||
const dataFile = await fs.readFile( |
||||
`${testsDir}/mysql-sakila-db/04-test-sakila-data.sql` |
||||
); |
||||
|
||||
await trx.raw( |
||||
schemaFile.toString().replace(/test_sakila/g, `test_sakila_${parallelId}`) |
||||
); |
||||
await trx.raw( |
||||
dataFile.toString().replace(/test_sakila/g, `test_sakila_${parallelId}`) |
||||
); |
||||
|
||||
await trx.commit(); |
||||
} catch (e) { |
||||
console.log('Error resetting mysql db', e); |
||||
await trx.rollback(e); |
||||
} |
||||
}; |
||||
|
||||
const resetMysqlSakilaProject = async ({ |
||||
token, |
||||
title, |
||||
parallelId, |
||||
oldProject, |
||||
isEmptyProject, |
||||
}: { |
||||
token: string; |
||||
title: string; |
||||
parallelId: string; |
||||
oldProject?: Project | undefined; |
||||
isEmptyProject: boolean; |
||||
}) => { |
||||
const knex = Knex(config); |
||||
|
||||
try { |
||||
await knex.raw(`USE test_sakila_${parallelId}`); |
||||
} catch (e) { |
||||
await knex.raw(`CREATE DATABASE test_sakila_${parallelId}`); |
||||
await knex.raw(`USE test_sakila_${parallelId}`); |
||||
} |
||||
|
||||
if ( |
||||
isEmptyProject || |
||||
(await isSakilaMysqlToBeReset(knex, parallelId, oldProject)) |
||||
) { |
||||
await resetSakilaMysql(knex, parallelId, isEmptyProject); |
||||
} |
||||
|
||||
const response = await axios.post( |
||||
'http://localhost:8080/api/v1/db/meta/projects/', |
||||
extMysqlProject(title, parallelId), |
||||
{ |
||||
headers: { |
||||
'xc-auth': token, |
||||
}, |
||||
} |
||||
); |
||||
if (response.status !== 200) { |
||||
console.error('Error creating project', response.data); |
||||
} |
||||
|
||||
await knex.destroy(); |
||||
}; |
||||
|
||||
export default resetMysqlSakilaProject; |
@ -0,0 +1,152 @@
|
||||
import axios from 'axios'; |
||||
import Knex from 'knex'; |
||||
|
||||
import { promises as fs } from 'fs'; |
||||
const util = require('util'); |
||||
const exec = util.promisify(require('child_process').exec); |
||||
|
||||
import Audit from '../../../models/Audit'; |
||||
import Project from '../../../models/Project'; |
||||
|
||||
const config = { |
||||
client: 'pg', |
||||
connection: { |
||||
host: 'localhost', |
||||
port: 5432, |
||||
user: 'postgres', |
||||
password: 'password', |
||||
database: 'postgres', |
||||
multipleStatements: true, |
||||
}, |
||||
searchPath: ['public', 'information_schema'], |
||||
pool: { min: 0, max: 5 }, |
||||
}; |
||||
|
||||
const extMysqlProject = (title, parallelId) => ({ |
||||
title, |
||||
bases: [ |
||||
{ |
||||
type: 'pg', |
||||
config: { |
||||
client: 'pg', |
||||
connection: { |
||||
host: 'localhost', |
||||
port: '5432', |
||||
user: 'postgres', |
||||
password: 'password', |
||||
database: `sakila_${parallelId}`, |
||||
}, |
||||
searchPath: ['public'], |
||||
}, |
||||
inflection_column: 'camelize', |
||||
inflection_table: 'camelize', |
||||
}, |
||||
], |
||||
external: true, |
||||
}); |
||||
|
||||
const isSakilaPgToBeReset = async (knex: Knex, project?: Project) => { |
||||
const tablesInDb: Array<string> = ( |
||||
await knex.raw( |
||||
`SELECT * FROM information_schema.tables WHERE table_schema = 'public'` |
||||
) |
||||
).rows.map((row) => row.table_name); |
||||
|
||||
if ( |
||||
tablesInDb.length === 0 || |
||||
(tablesInDb.length > 0 && !tablesInDb.includes(`actor`)) |
||||
) { |
||||
return true; |
||||
} |
||||
|
||||
if (!project) return false; |
||||
|
||||
const audits = await Audit.projectAuditList(project.id, {}); |
||||
|
||||
return audits?.length > 0; |
||||
}; |
||||
|
||||
const resetSakilaPg = async ( |
||||
pgknex: Knex, |
||||
parallelId: string, |
||||
isEmptyProject: boolean |
||||
) => { |
||||
const testsDir = __dirname.replace( |
||||
'/src/lib/services/test/TestResetService', |
||||
'/tests' |
||||
); |
||||
|
||||
await pgknex.raw(`DROP DATABASE IF EXISTS sakila_${parallelId}`); |
||||
await pgknex.raw(`CREATE DATABASE sakila_${parallelId}`); |
||||
|
||||
if (isEmptyProject) return; |
||||
|
||||
const sakilaKnex = Knex(sakilaKnexConfig(parallelId)); |
||||
|
||||
const schemaFile = await fs.readFile( |
||||
`${testsDir}/pg-sakila-db/03-postgres-sakila-schema.sql` |
||||
); |
||||
await sakilaKnex.raw(schemaFile.toString()); |
||||
|
||||
const dataFilePath = `${testsDir}/pg-sakila-db/04-postgres-sakila-insert-data.sql`; |
||||
await exec( |
||||
`export PGPASSWORD='${config.connection.password}';psql sakila_${parallelId} -h localhost -U postgres -w -f ${dataFilePath}` |
||||
); |
||||
|
||||
await sakilaKnex.destroy(); |
||||
}; |
||||
|
||||
const sakilaKnexConfig = (parallelId: string) => ({ |
||||
...config, |
||||
connection: { |
||||
...config.connection, |
||||
database: `sakila_${parallelId}`, |
||||
}, |
||||
}); |
||||
|
||||
const resetPgSakilaProject = async ({ |
||||
token, |
||||
title, |
||||
parallelId, |
||||
oldProject, |
||||
isEmptyProject, |
||||
}: { |
||||
token: string; |
||||
title: string; |
||||
parallelId: string; |
||||
oldProject?: Project | undefined; |
||||
isEmptyProject: boolean; |
||||
}) => { |
||||
const pgknex = Knex(config); |
||||
|
||||
try { |
||||
await pgknex.raw(`CREATE DATABASE sakila_${parallelId}`); |
||||
} catch (e) {} |
||||
|
||||
const sakilaKnex = Knex(sakilaKnexConfig(parallelId)); |
||||
|
||||
if (isEmptyProject || (await isSakilaPgToBeReset(sakilaKnex, oldProject))) { |
||||
await sakilaKnex.destroy(); |
||||
await resetSakilaPg(pgknex, parallelId, isEmptyProject); |
||||
} else { |
||||
await sakilaKnex.destroy(); |
||||
} |
||||
|
||||
const response = await axios.post( |
||||
'http://localhost:8080/api/v1/db/meta/projects/', |
||||
extMysqlProject(title, parallelId), |
||||
{ |
||||
headers: { |
||||
'xc-auth': token, |
||||
}, |
||||
} |
||||
); |
||||
if (response.status !== 200) { |
||||
console.error('Error creating project', response.data); |
||||
throw new Error('Error creating project', response.data); |
||||
} |
||||
|
||||
await pgknex.destroy(); |
||||
}; |
||||
|
||||
export default resetPgSakilaProject; |
@ -0,0 +1,42 @@
|
||||
import axios from 'axios'; |
||||
import cors from 'cors'; |
||||
import express from 'express'; |
||||
|
||||
import Noco from '../lib/Noco'; |
||||
import User from '../lib/models/User'; |
||||
process.env.NC_VERSION = '0009044'; |
||||
|
||||
const server = express(); |
||||
server.enable('trust proxy'); |
||||
server.disable('etag'); |
||||
server.disable('x-powered-by'); |
||||
server.use( |
||||
cors({ |
||||
exposedHeaders: 'xc-db-response', |
||||
}) |
||||
); |
||||
|
||||
server.set('view engine', 'ejs'); |
||||
|
||||
process.env[`DEBUG`] = 'xc*'; |
||||
|
||||
(async () => { |
||||
const httpServer = server.listen(process.env.PORT || 8080, () => { |
||||
console.log(`App started successfully.\nVisit -> ${Noco.dashboardUrl}`); |
||||
}); |
||||
server.use(await Noco.init({}, httpServer, server)); |
||||
|
||||
// Wait for 0.5 seconds for the server to start
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); |
||||
|
||||
if (!(await User.getByEmail('user@nocodb.com'))) { |
||||
const response = await axios.post( |
||||
`http://localhost:${process.env.PORT || 8080}/api/v1/auth/user/signup`, |
||||
{ |
||||
email: 'user@nocodb.com', |
||||
password: 'Password123.', |
||||
} |
||||
); |
||||
console.log(response.data); |
||||
} |
||||
})().catch((e) => console.log(e)); |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,604 @@
|
||||
/* |
||||
|
||||
Sakila for SQLite is a port of the Sakila example database available for MySQL, which was originally developed by Mike Hillyer of the MySQL AB documentation team. |
||||
This project is designed to help database administrators to decide which database to use for development of new products |
||||
The user can run the same SQL against different kind of databases and compare the performance |
||||
|
||||
License: BSD |
||||
Copyright DB Software Laboratory |
||||
http://www.etl-tools.com |
||||
|
||||
*/ |
||||
|
||||
|
||||
CREATE TABLE prefix___actor ( |
||||
actor_id numeric NOT NULL , |
||||
first_name VARCHAR(45) NOT NULL, |
||||
last_name VARCHAR(45) NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (actor_id) |
||||
) ; |
||||
|
||||
CREATE INDEX prefix___idx_actor_last_name ON prefix___actor(last_name) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___actor_trigger_ai AFTER INSERT ON prefix___actor |
||||
BEGIN |
||||
UPDATE prefix___actor SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___actor_trigger_au AFTER UPDATE ON prefix___actor |
||||
BEGIN |
||||
UPDATE prefix___actor SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table country |
||||
-- |
||||
|
||||
CREATE TABLE prefix___country ( |
||||
country_id SMALLINT NOT NULL, |
||||
country VARCHAR(50) NOT NULL, |
||||
last_update TIMESTAMP, |
||||
PRIMARY KEY (country_id) |
||||
) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___country_trigger_ai AFTER INSERT ON prefix___country |
||||
BEGIN |
||||
UPDATE prefix___country SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___country_trigger_au AFTER UPDATE ON prefix___country |
||||
BEGIN |
||||
UPDATE prefix___country SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table city |
||||
-- |
||||
|
||||
CREATE TABLE prefix___city ( |
||||
city_id int NOT NULL, |
||||
city VARCHAR(50) NOT NULL, |
||||
country_id SMALLINT NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (city_id), |
||||
CONSTRAINT prefix___fk_city_country FOREIGN KEY (country_id) REFERENCES prefix___country (country_id) ON DELETE NO ACTION ON UPDATE CASCADE |
||||
) |
||||
; |
||||
CREATE INDEX prefix___idx_fk_country_id ON prefix___city(country_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___city_trigger_ai AFTER INSERT ON prefix___city |
||||
BEGIN |
||||
UPDATE prefix___city SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___city_trigger_au AFTER UPDATE ON prefix___city |
||||
BEGIN |
||||
UPDATE prefix___city SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table address |
||||
-- |
||||
|
||||
CREATE TABLE prefix___address ( |
||||
address_id int NOT NULL, |
||||
address VARCHAR(50) NOT NULL, |
||||
address2 VARCHAR(50) DEFAULT NULL, |
||||
district VARCHAR(20) NOT NULL, |
||||
city_id INT NOT NULL, |
||||
postal_code VARCHAR(10) DEFAULT NULL, |
||||
phone VARCHAR(20) NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (address_id), |
||||
CONSTRAINT prefix___fk_address_city FOREIGN KEY (city_id) REFERENCES prefix___city (city_id) ON DELETE NO ACTION ON UPDATE CASCADE |
||||
) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_fk_city_id ON prefix___address(city_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___address_trigger_ai AFTER INSERT ON prefix___address |
||||
BEGIN |
||||
UPDATE prefix___address SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___address_trigger_au AFTER UPDATE ON prefix___address |
||||
BEGIN |
||||
UPDATE prefix___address SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table language |
||||
-- |
||||
|
||||
CREATE TABLE prefix___language ( |
||||
language_id SMALLINT NOT NULL , |
||||
name CHAR(20) NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (language_id) |
||||
) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___language_trigger_ai AFTER INSERT ON prefix___language |
||||
BEGIN |
||||
UPDATE prefix___language SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___language_trigger_au AFTER UPDATE ON prefix___language |
||||
BEGIN |
||||
UPDATE prefix___language SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table category |
||||
-- |
||||
|
||||
CREATE TABLE prefix___category ( |
||||
category_id SMALLINT NOT NULL, |
||||
name VARCHAR(25) NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (category_id) |
||||
); |
||||
|
||||
CREATE TRIGGER prefix___category_trigger_ai AFTER INSERT ON prefix___category |
||||
BEGIN |
||||
UPDATE prefix___category SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___category_trigger_au AFTER UPDATE ON prefix___category |
||||
BEGIN |
||||
UPDATE prefix___category SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table customer |
||||
-- |
||||
|
||||
CREATE TABLE prefix___customer ( |
||||
customer_id INT NOT NULL, |
||||
store_id INT NOT NULL, |
||||
first_name VARCHAR(45) NOT NULL, |
||||
last_name VARCHAR(45) NOT NULL, |
||||
email VARCHAR(50) DEFAULT NULL, |
||||
address_id INT NOT NULL, |
||||
active CHAR(1) DEFAULT 'Y' NOT NULL, |
||||
create_date TIMESTAMP NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (customer_id), |
||||
CONSTRAINT prefix___fk_customer_store FOREIGN KEY (store_id) REFERENCES prefix___store (store_id) ON DELETE NO ACTION ON UPDATE CASCADE, |
||||
CONSTRAINT prefix___fk_customer_address FOREIGN KEY (address_id) REFERENCES prefix___address (address_id) ON DELETE NO ACTION ON UPDATE CASCADE |
||||
) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_customer_fk_store_id ON prefix___customer(store_id) |
||||
; |
||||
CREATE INDEX prefix___idx_customer_fk_address_id ON prefix___customer(address_id) |
||||
; |
||||
CREATE INDEX prefix___idx_customer_last_name ON prefix___customer(last_name) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___customer_trigger_ai AFTER INSERT ON prefix___customer |
||||
BEGIN |
||||
UPDATE prefix___customer SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___customer_trigger_au AFTER UPDATE ON prefix___customer |
||||
BEGIN |
||||
UPDATE prefix___customer SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table film |
||||
-- |
||||
|
||||
CREATE TABLE prefix___film ( |
||||
film_id int NOT NULL, |
||||
title VARCHAR(255) NOT NULL, |
||||
description BLOB SUB_TYPE TEXT DEFAULT NULL, |
||||
release_year VARCHAR(4) DEFAULT NULL, |
||||
language_id SMALLINT NOT NULL, |
||||
original_language_id SMALLINT DEFAULT NULL, |
||||
rental_duration SMALLINT DEFAULT 3 NOT NULL, |
||||
rental_rate DECIMAL(4,2) DEFAULT 4.99 NOT NULL, |
||||
length SMALLINT DEFAULT NULL, |
||||
replacement_cost DECIMAL(5,2) DEFAULT 19.99 NOT NULL, |
||||
rating VARCHAR(10) DEFAULT 'G', |
||||
special_features VARCHAR(100) DEFAULT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (film_id), |
||||
CONSTRAINT CHECK_special_features CHECK(special_features is null or |
||||
special_features like '%Trailers%' or |
||||
special_features like '%Commentaries%' or |
||||
special_features like '%Deleted Scenes%' or |
||||
special_features like '%Behind the Scenes%'), |
||||
CONSTRAINT CHECK_special_rating CHECK(rating in ('G','PG','PG-13','R','NC-17')), |
||||
CONSTRAINT prefix___fk_film_language FOREIGN KEY (language_id) REFERENCES prefix___language (language_id) , |
||||
CONSTRAINT prefix___fk_film_language_original FOREIGN KEY (original_language_id) REFERENCES prefix___language (language_id) |
||||
) |
||||
; |
||||
CREATE INDEX prefix___idx_fk_language_id ON prefix___film(language_id) |
||||
; |
||||
CREATE INDEX prefix___idx_fk_original_language_id ON prefix___film(original_language_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___film_trigger_ai AFTER INSERT ON prefix___film |
||||
BEGIN |
||||
UPDATE prefix___film SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___film_trigger_au AFTER UPDATE ON prefix___film |
||||
BEGIN |
||||
UPDATE prefix___film SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table film_actor |
||||
-- |
||||
|
||||
CREATE TABLE prefix___film_actor ( |
||||
actor_id INT NOT NULL, |
||||
film_id INT NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (actor_id,film_id), |
||||
CONSTRAINT prefix___fk_film_actor_actor FOREIGN KEY (actor_id) REFERENCES prefix___actor (actor_id) ON DELETE NO ACTION ON UPDATE CASCADE, |
||||
CONSTRAINT prefix___fk_film_actor_film FOREIGN KEY (film_id) REFERENCES prefix___film (film_id) ON DELETE NO ACTION ON UPDATE CASCADE |
||||
) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_fk_film_actor_film ON prefix___film_actor(film_id) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_fk_film_actor_actor ON prefix___film_actor(actor_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___film_actor_trigger_ai AFTER INSERT ON prefix___film_actor |
||||
BEGIN |
||||
UPDATE prefix___film_actor SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___film_actor_trigger_au AFTER UPDATE ON prefix___film_actor |
||||
BEGIN |
||||
UPDATE prefix___film_actor SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
|
||||
-- |
||||
-- Table structure for table film_category |
||||
-- |
||||
|
||||
CREATE TABLE prefix___film_category ( |
||||
film_id INT NOT NULL, |
||||
category_id SMALLINT NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (film_id, category_id), |
||||
CONSTRAINT prefix___fk_film_category_film FOREIGN KEY (film_id) REFERENCES prefix___film (film_id) ON DELETE NO ACTION ON UPDATE CASCADE, |
||||
CONSTRAINT prefix___fk_film_category_category FOREIGN KEY (category_id) REFERENCES prefix___category (category_id) ON DELETE NO ACTION ON UPDATE CASCADE |
||||
) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_fk_film_category_film ON prefix___film_category(film_id) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_fk_film_category_category ON prefix___film_category(category_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___film_category_trigger_ai AFTER INSERT ON prefix___film_category |
||||
BEGIN |
||||
UPDATE prefix___film_category SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___film_category_trigger_au AFTER UPDATE ON prefix___film_category |
||||
BEGIN |
||||
UPDATE prefix___film_category SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table film_text |
||||
-- |
||||
|
||||
CREATE TABLE prefix___film_text ( |
||||
film_id SMALLINT NOT NULL, |
||||
title VARCHAR(255) NOT NULL, |
||||
description BLOB SUB_TYPE TEXT, |
||||
PRIMARY KEY (film_id) |
||||
) |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table inventory |
||||
-- |
||||
|
||||
CREATE TABLE prefix___inventory ( |
||||
inventory_id INT NOT NULL, |
||||
film_id INT NOT NULL, |
||||
store_id INT NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (inventory_id), |
||||
CONSTRAINT prefix___fk_inventory_store FOREIGN KEY (store_id) REFERENCES prefix___store (store_id) ON DELETE NO ACTION ON UPDATE CASCADE, |
||||
CONSTRAINT prefix___fk_inventory_film FOREIGN KEY (film_id) REFERENCES prefix___film (film_id) ON DELETE NO ACTION ON UPDATE CASCADE |
||||
) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_fk_film_id ON prefix___inventory(film_id) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_fk_film_id_store_id ON prefix___inventory(store_id,film_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___inventory_trigger_ai AFTER INSERT ON prefix___inventory |
||||
BEGIN |
||||
UPDATE prefix___inventory SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___inventory_trigger_au AFTER UPDATE ON prefix___inventory |
||||
BEGIN |
||||
UPDATE prefix___inventory SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table staff |
||||
-- |
||||
|
||||
CREATE TABLE prefix___staff ( |
||||
staff_id SMALLINT NOT NULL, |
||||
first_name VARCHAR(45) NOT NULL, |
||||
last_name VARCHAR(45) NOT NULL, |
||||
address_id INT NOT NULL, |
||||
picture BLOB DEFAULT NULL, |
||||
email VARCHAR(50) DEFAULT NULL, |
||||
store_id INT NOT NULL, |
||||
active SMALLINT DEFAULT 1 NOT NULL, |
||||
username VARCHAR(16) NOT NULL, |
||||
password VARCHAR(40) DEFAULT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (staff_id), |
||||
CONSTRAINT prefix___fk_staff_store FOREIGN KEY (store_id) REFERENCES prefix___store (store_id) ON DELETE NO ACTION ON UPDATE CASCADE, |
||||
CONSTRAINT prefix___fk_staff_address FOREIGN KEY (address_id) REFERENCES prefix___address (address_id) ON DELETE NO ACTION ON UPDATE CASCADE |
||||
) |
||||
; |
||||
CREATE INDEX prefix___idx_fk_staff_store_id ON prefix___staff(store_id) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_fk_staff_address_id ON prefix___staff(address_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___staff_trigger_ai AFTER INSERT ON prefix___staff |
||||
BEGIN |
||||
UPDATE prefix___staff SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___staff_trigger_au AFTER UPDATE ON prefix___staff |
||||
BEGIN |
||||
UPDATE prefix___staff SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table store |
||||
-- |
||||
|
||||
CREATE TABLE prefix___store ( |
||||
store_id INT NOT NULL, |
||||
manager_staff_id SMALLINT NOT NULL, |
||||
address_id INT NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (store_id), |
||||
CONSTRAINT prefix___fk_store_staff FOREIGN KEY (manager_staff_id) REFERENCES prefix___staff (staff_id) , |
||||
CONSTRAINT prefix___fk_store_address FOREIGN KEY (address_id) REFERENCES prefix___address (address_id) |
||||
) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_store_fk_manager_staff_id ON prefix___store(manager_staff_id) |
||||
; |
||||
|
||||
CREATE INDEX prefix___idx_fk_store_address ON prefix___store(address_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___store_trigger_ai AFTER INSERT ON prefix___store |
||||
BEGIN |
||||
UPDATE prefix___store SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___store_trigger_au AFTER UPDATE ON prefix___store |
||||
BEGIN |
||||
UPDATE prefix___store SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
-- |
||||
-- Table structure for table payment |
||||
-- |
||||
|
||||
CREATE TABLE prefix___payment ( |
||||
payment_id int NOT NULL, |
||||
customer_id INT NOT NULL, |
||||
staff_id SMALLINT NOT NULL, |
||||
rental_id INT DEFAULT NULL, |
||||
amount DECIMAL(5,2) NOT NULL, |
||||
payment_date TIMESTAMP NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (payment_id), |
||||
CONSTRAINT prefix___fk_payment_rental FOREIGN KEY (rental_id) REFERENCES prefix___rental (rental_id) ON DELETE SET NULL ON UPDATE CASCADE, |
||||
CONSTRAINT prefix___fk_payment_customer FOREIGN KEY (customer_id) REFERENCES prefix___customer (customer_id) , |
||||
CONSTRAINT prefix___fk_payment_staff FOREIGN KEY (staff_id) REFERENCES prefix___staff (staff_id) |
||||
) |
||||
; |
||||
CREATE INDEX prefix___idx_fk_staff_id ON prefix___payment(staff_id) |
||||
; |
||||
CREATE INDEX prefix___idx_fk_customer_id ON prefix___payment(customer_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___payment_trigger_ai AFTER INSERT ON prefix___payment |
||||
BEGIN |
||||
UPDATE prefix___payment SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___payment_trigger_au AFTER UPDATE ON prefix___payment |
||||
BEGIN |
||||
UPDATE prefix___payment SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TABLE prefix___rental ( |
||||
rental_id INT NOT NULL, |
||||
rental_date TIMESTAMP NOT NULL, |
||||
inventory_id INT NOT NULL, |
||||
customer_id INT NOT NULL, |
||||
return_date TIMESTAMP DEFAULT NULL, |
||||
staff_id SMALLINT NOT NULL, |
||||
last_update TIMESTAMP NOT NULL, |
||||
PRIMARY KEY (rental_id), |
||||
CONSTRAINT prefix___fk_rental_staff FOREIGN KEY (staff_id) REFERENCES prefix___staff (staff_id) , |
||||
CONSTRAINT prefix___fk_rental_inventory FOREIGN KEY (inventory_id) REFERENCES prefix___inventory (inventory_id) , |
||||
CONSTRAINT prefix___fk_rental_customer FOREIGN KEY (customer_id) REFERENCES prefix___customer (customer_id) |
||||
) |
||||
; |
||||
CREATE INDEX prefix___idx_rental_fk_inventory_id ON prefix___rental(inventory_id) |
||||
; |
||||
CREATE INDEX prefix___idx_rental_fk_customer_id ON prefix___rental(customer_id) |
||||
; |
||||
CREATE INDEX prefix___idx_rental_fk_staff_id ON prefix___rental(staff_id) |
||||
; |
||||
CREATE UNIQUE INDEX prefix___idx_rental_uq ON prefix___rental (rental_date,inventory_id,customer_id) |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___rental_trigger_ai AFTER INSERT ON prefix___rental |
||||
BEGIN |
||||
UPDATE prefix___rental SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
CREATE TRIGGER prefix___rental_trigger_au AFTER UPDATE ON prefix___rental |
||||
BEGIN |
||||
UPDATE prefix___rental SET last_update = DATETIME('NOW') WHERE rowid = new.rowid |
||||
END |
||||
; |
||||
|
||||
|
||||
-- |
||||
-- View structure for view customer_list |
||||
-- |
||||
|
||||
CREATE VIEW prefix___customer_list |
||||
AS |
||||
SELECT cu.customer_id AS ID, |
||||
cu.first_name||' '||cu.last_name AS name, |
||||
a.address AS address, |
||||
a.postal_code AS zip_code, |
||||
a.phone AS phone, |
||||
prefix___city.city AS city, |
||||
prefix___country.country AS country, |
||||
case when cu.active=1 then 'active' else '' end AS notes, |
||||
cu.store_id AS SID |
||||
FROM prefix___customer AS cu JOIN prefix___address AS a ON cu.address_id = a.address_id JOIN prefix___city ON a.city_id = prefix___city.city_id |
||||
JOIN prefix___country ON prefix___city.country_id = prefix___country.country_id |
||||
; |
||||
-- |
||||
-- View structure for view film_list |
||||
-- |
||||
|
||||
CREATE VIEW prefix___film_list |
||||
AS |
||||
SELECT prefix___film.film_id AS FID, |
||||
prefix___film.title AS title, |
||||
prefix___film.description AS description, |
||||
prefix___category.name AS category, |
||||
prefix___film.rental_rate AS price, |
||||
prefix___film.length AS length, |
||||
prefix___film.rating AS rating, |
||||
prefix___actor.first_name||' '||prefix___actor.last_name AS actors |
||||
FROM prefix___category LEFT JOIN prefix___film_category ON prefix___category.category_id = prefix___film_category.category_id LEFT JOIN prefix___film ON prefix___film_category.film_id = prefix___film.film_id |
||||
JOIN prefix___film_actor ON prefix___film.film_id = prefix___film_actor.film_id |
||||
JOIN prefix___actor ON prefix___film_actor.actor_id = prefix___actor.actor_id |
||||
; |
||||
|
||||
-- |
||||
-- View structure for view staff_list |
||||
-- |
||||
|
||||
CREATE VIEW prefix___staff_list |
||||
AS |
||||
SELECT s.staff_id AS ID, |
||||
s.first_name||' '||s.last_name AS name, |
||||
a.address AS address, |
||||
a.postal_code AS zip_code, |
||||
a.phone AS phone, |
||||
prefix___city.city AS city, |
||||
prefix___country.country AS country, |
||||
s.store_id AS SID |
||||
FROM prefix___staff AS s JOIN prefix___address AS a ON s.address_id = a.address_id JOIN prefix___city ON a.city_id = prefix___city.city_id |
||||
JOIN prefix___country ON prefix___city.country_id = prefix___country.country_id |
||||
; |
||||
-- |
||||
-- View structure for view sales_by_store |
||||
-- |
||||
|
||||
CREATE VIEW prefix___sales_by_store |
||||
AS |
||||
SELECT |
||||
s.store_id |
||||
,c.city||','||cy.country AS store |
||||
,m.first_name||' '||m.last_name AS manager |
||||
,SUM(p.amount) AS total_sales |
||||
FROM prefix___payment AS p |
||||
INNER JOIN prefix___rental AS r ON p.rental_id = r.rental_id |
||||
INNER JOIN prefix___inventory AS i ON r.inventory_id = i.inventory_id |
||||
INNER JOIN prefix___store AS s ON i.store_id = s.store_id |
||||
INNER JOIN prefix___address AS a ON s.address_id = a.address_id |
||||
INNER JOIN prefix___city AS c ON a.city_id = c.city_id |
||||
INNER JOIN prefix___country AS cy ON c.country_id = cy.country_id |
||||
INNER JOIN prefix___staff AS m ON s.manager_staff_id = m.staff_id |
||||
GROUP BY |
||||
s.store_id |
||||
, c.city||','||cy.country |
||||
, m.first_name||' '||m.last_name |
||||
; |
||||
-- |
||||
-- View structure for view sales_by_film_category |
||||
-- |
||||
-- Note that total sales will add up to >100% because |
||||
-- some titles belong to more than 1 category |
||||
-- |
||||
|
||||
CREATE VIEW prefix___sales_by_film_category |
||||
AS |
||||
SELECT |
||||
c.name AS category |
||||
, SUM(p.amount) AS total_sales |
||||
FROM prefix___payment AS p |
||||
INNER JOIN prefix___rental AS r ON p.rental_id = r.rental_id |
||||
INNER JOIN prefix___inventory AS i ON r.inventory_id = i.inventory_id |
||||
INNER JOIN prefix___film AS f ON i.film_id = f.film_id |
||||
INNER JOIN prefix___film_category AS fc ON f.film_id = fc.film_id |
||||
INNER JOIN prefix___category AS c ON fc.category_id = c.category_id |
||||
GROUP BY c.name |
||||
; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,70 @@
|
||||
{ |
||||
"root": true, |
||||
"parser": "@typescript-eslint/parser", |
||||
"parserOptions": { |
||||
"project": "./tsconfig.json" |
||||
}, |
||||
"env": { |
||||
"es6": true |
||||
}, |
||||
"ignorePatterns": [ |
||||
"node_modules", |
||||
"build", |
||||
"coverage", |
||||
"playwright-report", |
||||
"output", |
||||
"dist", |
||||
"nc", |
||||
"tsconfig.json", |
||||
".eslintrc.json" |
||||
], |
||||
"plugins": [ |
||||
"import", |
||||
"eslint-comments", |
||||
"functional" |
||||
], |
||||
"extends": [ |
||||
"eslint:recommended", |
||||
"plugin:eslint-comments/recommended", |
||||
"plugin:@typescript-eslint/recommended", |
||||
"plugin:import/typescript", |
||||
"plugin:prettier/recommended", |
||||
"plugin:json/recommended" |
||||
], |
||||
"globals": { |
||||
"BigInt": true, |
||||
"console": true, |
||||
"WebAssembly": true, |
||||
"window": true, |
||||
"document": true, |
||||
"localStorage": true |
||||
}, |
||||
"rules": { |
||||
"@typescript-eslint/no-floating-promises": ["error"], |
||||
"@typescript-eslint/explicit-module-boundary-types": "off", |
||||
"eslint-comments/disable-enable-pair": [ |
||||
"error", |
||||
{ |
||||
"allowWholeFile": true |
||||
} |
||||
], |
||||
"eslint-comments/no-unused-disable": "error", |
||||
"sort-imports": [ |
||||
"error", |
||||
{ |
||||
"ignoreDeclarationSort": true, |
||||
"ignoreCase": true |
||||
} |
||||
], |
||||
"@typescript-eslint/no-this-alias": "off", |
||||
|
||||
"@typescript-eslint/ban-ts-comment": "off", |
||||
"@typescript-eslint/no-explicit-any": "off", |
||||
"@typescript-eslint/no-unused-vars": "off", |
||||
"@typescript-eslint/no-var-requires": "off", |
||||
"no-useless-catch": "off", |
||||
"no-empty": "off", |
||||
"@typescript-eslint/no-empty-function": "off", |
||||
"import/order": "off" |
||||
} |
||||
} |
@ -0,0 +1,8 @@
|
||||
node_modules/ |
||||
/test-results/ |
||||
/playwright-report/ |
||||
/playwright-report copy/ |
||||
/playwright/.cache/ |
||||
.env |
||||
output |
||||
/output copy/ |
@ -0,0 +1,3 @@
|
||||
{ |
||||
"**/*.{ts,tsx,js,json}": "npx eslint --fix" |
||||
} |
@ -0,0 +1,2 @@
|
||||
# package.json is formatted by package managers, so we ignore it here |
||||
package.json |
@ -0,0 +1,7 @@
|
||||
module.exports = { |
||||
"trailingComma": "es5", |
||||
"arrowParens": "avoid", |
||||
singleQuote: true, |
||||
tabWidth: 2, |
||||
printWidth: 120 |
||||
}; |
@ -0,0 +1,4 @@
|
||||
const airtableApiKey = 'keyn1MR87qgyUsYg4'; |
||||
const airtableApiBase = 'https://airtable.com/shr4z0qmh6dg5s3eB'; |
||||
|
||||
export { airtableApiKey, airtableApiBase }; |
@ -0,0 +1,110 @@
|
||||
Country,LastUpdate,City List |
||||
Afghanistan,2006-02-15 04:44:00,Kabul |
||||
Algeria,2006-02-15 04:44:00,"Batna, Bchar, Skikda" |
||||
American Samoa,2006-02-15 04:44:00,Tafuna |
||||
Angola,2006-02-15 04:44:00,"Benguela, Namibe" |
||||
Anguilla,2006-02-15 04:44:00,South Hill |
||||
Argentina,2006-02-15 04:44:00,"Almirante Brown, Avellaneda, Baha Blanca, Crdoba, Escobar, Ezeiza, La Plata, Merlo, Quilmes, San Miguel de Tucumn, Santa F, Tandil, Vicente Lpez" |
||||
Armenia,2006-02-15 04:44:00,Yerevan |
||||
Australia,2006-02-15 04:44:00,Woodridge |
||||
Austria,2006-02-15 04:44:00,"Graz, Linz, Salzburg" |
||||
Azerbaijan,2006-02-15 04:44:00,"Baku, Sumqayit" |
||||
Bahrain,2006-02-15 04:44:00,al-Manama |
||||
Bangladesh,2006-02-15 04:44:00,"Dhaka, Jamalpur, Tangail" |
||||
Belarus,2006-02-15 04:44:00,"Mogiljov, Molodetno" |
||||
Bolivia,2006-02-15 04:44:00,"El Alto, Sucre" |
||||
Brazil,2006-02-15 04:44:00,"Alvorada, Angra dos Reis, Anpolis, Aparecida de Goinia, Araatuba, Bag, Belm, Blumenau, Boa Vista, Braslia, Goinia, Guaruj, guas Lindas de Gois, Ibirit, Juazeiro do Norte, Juiz de Fora, Luzinia, Maring, Po, Poos de Caldas, Rio Claro, Santa Brbara dOeste, Santo Andr, So Bernardo do Campo, So Leopoldo" |
||||
Brunei,2006-02-15 04:44:00,Bandar Seri Begawan |
||||
Bulgaria,2006-02-15 04:44:00,"Ruse, Stara Zagora" |
||||
Cambodia,2006-02-15 04:44:00,"Battambang, Phnom Penh" |
||||
Cameroon,2006-02-15 04:44:00,"Bamenda, Yaound" |
||||
Canada,2006-02-15 04:44:00,"Gatineau, Halifax, Lethbridge, London, Oshawa, Richmond Hill, Vancouver" |
||||
Chad,2006-02-15 04:44:00,NDjamna |
||||
Chile,2006-02-15 04:44:00,"Antofagasta, Coquimbo, Rancagua" |
||||
China,2006-02-15 04:44:00,"Baicheng, Baiyin, Binzhou, Changzhou, Datong, Daxian, Dongying, Emeishan, Enshi, Ezhou, Fuyu, Fuzhou, Haining, Hami, Hohhot, Huaian, Jinchang, Jining, Jinzhou, Junan, Korla, Laiwu, Laohekou, Lengshuijiang, Leshan" |
||||
Colombia,2006-02-15 04:44:00,"Buenaventura, Dos Quebradas, Florencia, Pereira, Sincelejo, Sogamoso" |
||||
"Congo, The Democratic Republic of the",2006-02-15 04:44:00,"Lubumbashi, Mwene-Ditu" |
||||
Czech Republic,2006-02-15 04:44:00,Olomouc |
||||
Dominican Republic,2006-02-15 04:44:00,"La Romana, San Felipe de Puerto Plata, Santiago de los Caballeros" |
||||
Ecuador,2006-02-15 04:44:00,"Loja, Portoviejo, Robamba" |
||||
Egypt,2006-02-15 04:44:00,"Bilbays, Idfu, Mit Ghamr, Qalyub, Sawhaj, Shubra al-Khayma" |
||||
Estonia,2006-02-15 04:44:00,Tartu |
||||
Ethiopia,2006-02-15 04:44:00,Addis Abeba |
||||
Faroe Islands,2006-02-15 04:44:00,Trshavn |
||||
Finland,2006-02-15 04:44:00,Oulu |
||||
France,2006-02-15 04:44:00,"Brest, Le Mans, Toulon, Toulouse" |
||||
French Guiana,2006-02-15 04:44:00,Cayenne |
||||
French Polynesia,2006-02-15 04:44:00,"Faaa, Papeete" |
||||
Gambia,2006-02-15 04:44:00,Banjul |
||||
Germany,2006-02-15 04:44:00,"Duisburg, Erlangen, Halle/Saale, Mannheim, Saarbrcken, Siegen, Witten" |
||||
Greece,2006-02-15 04:44:00,"Athenai, Patras" |
||||
Greenland,2006-02-15 04:44:00,Nuuk |
||||
Holy See (Vatican City State),2006-02-15 04:44:00,Citt del Vaticano |
||||
Hong Kong,2006-02-15 04:44:00,Kowloon and New Kowloon |
||||
Hungary,2006-02-15 04:44:00,Szkesfehrvr |
||||
India,2006-02-15 04:44:00,"Adoni, Ahmadnagar, Allappuzha (Alleppey), Ambattur, Amroha, Balurghat, Berhampore (Baharampur), Bhavnagar, Bhilwara, Bhimavaram, Bhopal, Bhusawal, Bijapur, Chandrapur, Chapra, Dhule (Dhulia), Etawah, Firozabad, Gandhinagar, Gulbarga, Haldia, Halisahar, Hoshiarpur, Hubli-Dharwad, Jaipur" |
||||
Indonesia,2006-02-15 04:44:00,"Cianjur, Ciomas, Ciparay, Gorontalo, Jakarta, Lhokseumawe, Madiun, Pangkal Pinang, Pemalang, Pontianak, Probolinggo, Purwakarta, Surakarta, Tegal" |
||||
Iran,2006-02-15 04:44:00,"Arak, Esfahan, Kermanshah, Najafabad, Qomsheh, Shahr-e Kord, Sirjan, Tabriz" |
||||
Iraq,2006-02-15 04:44:00,Mosul |
||||
Israel,2006-02-15 04:44:00,"Ashdod, Ashqelon, Bat Yam, Tel Aviv-Jaffa" |
||||
Italy,2006-02-15 04:44:00,"Alessandria, Bergamo, Brescia, Brindisi, Livorno, Syrakusa, Udine" |
||||
Japan,2006-02-15 04:44:00,"Akishima, Fukuyama, Higashiosaka, Hino, Hiroshima, Isesaki, Iwaki, Iwakuni, Iwatsuki, Izumisano, Kakamigahara, Kamakura, Kanazawa, Koriyama, Kurashiki, Kuwana, Matsue, Miyakonojo, Nagareyama, Okayama, Okinawa, Omiya, Onomichi, Otsu, Sagamihara" |
||||
Kazakstan,2006-02-15 04:44:00,"Pavlodar, Zhezqazghan" |
||||
Kenya,2006-02-15 04:44:00,"Kisumu, Nyeri" |
||||
Kuwait,2006-02-15 04:44:00,Jalib al-Shuyukh |
||||
Latvia,2006-02-15 04:44:00,"Daugavpils, Liepaja" |
||||
Liechtenstein,2006-02-15 04:44:00,Vaduz |
||||
Lithuania,2006-02-15 04:44:00,Vilnius |
||||
Madagascar,2006-02-15 04:44:00,Mahajanga |
||||
Malawi,2006-02-15 04:44:00,Lilongwe |
||||
Malaysia,2006-02-15 04:44:00,"Ipoh, Kuching, Sungai Petani" |
||||
Mexico,2006-02-15 04:44:00,"Acua, Allende, Atlixco, Carmen, Celaya, Coacalco de Berriozbal, Coatzacoalcos, Cuauhtmoc, Cuautla, Cuernavaca, El Fuerte, Guadalajara, Hidalgo, Huejutla de Reyes, Huixquilucan, Jos Azueta, Jurez, La Paz, Matamoros, Mexicali, Monclova, Nezahualcyotl, Pachuca de Soto, Salamanca, San Felipe del Progreso" |
||||
Moldova,2006-02-15 04:44:00,Chisinau |
||||
Morocco,2006-02-15 04:44:00,"Beni-Mellal, Nador, Sal" |
||||
Mozambique,2006-02-15 04:44:00,"Beira, Naala-Porto, Tete" |
||||
Myanmar,2006-02-15 04:44:00,"Monywa, Myingyan" |
||||
Nauru,2006-02-15 04:44:00,Yangor |
||||
Nepal,2006-02-15 04:44:00,Birgunj |
||||
Netherlands,2006-02-15 04:44:00,"Amersfoort, Apeldoorn, Ede, Emmen, s-Hertogenbosch" |
||||
New Zealand,2006-02-15 04:44:00,Hamilton |
||||
Nigeria,2006-02-15 04:44:00,"Benin City, Deba Habe, Effon-Alaiye, Ife, Ikerre, Ilorin, Kaduna, Ogbomosho, Ondo, Owo, Oyo, Sokoto, Zaria" |
||||
North Korea,2006-02-15 04:44:00,Pyongyang |
||||
Oman,2006-02-15 04:44:00,"Masqat, Salala" |
||||
Pakistan,2006-02-15 04:44:00,"Dadu, Mandi Bahauddin, Mardan, Okara, Shikarpur" |
||||
Paraguay,2006-02-15 04:44:00,"Asuncin, Ciudad del Este, San Lorenzo" |
||||
Peru,2006-02-15 04:44:00,"Callao, Hunuco, Lima, Sullana" |
||||
Philippines,2006-02-15 04:44:00,"Baybay, Bayugan, Bislig, Cabuyao, Cavite, Davao, Gingoog, Hagonoy, Iligan, Imus, Lapu-Lapu, Mandaluyong, Ozamis, Santa Rosa, Taguig, Talavera, Tanauan, Tanza, Tarlac, Tuguegarao" |
||||
Poland,2006-02-15 04:44:00,"Bydgoszcz, Czestochowa, Jastrzebie-Zdrj, Kalisz, Lublin, Plock, Tychy, Wroclaw" |
||||
Puerto Rico,2006-02-15 04:44:00,"Arecibo, Ponce" |
||||
Romania,2006-02-15 04:44:00,"Botosani, Bucuresti" |
||||
Runion,2006-02-15 04:44:00,Saint-Denis |
||||
Russian Federation,2006-02-15 04:44:00,"Atinsk, Balaiha, Dzerzinsk, Elista, Ivanovo, Jaroslavl, Jelets, Kaliningrad, Kamyin, Kirovo-Tepetsk, Kolpino, Korolev, Kurgan, Kursk, Lipetsk, Ljubertsy, Maikop, Moscow, Nabereznyje Telny, Niznekamsk, Novoterkassk, Pjatigorsk, Serpuhov, Smolensk, Syktyvkar" |
||||
Saint Vincent and the Grenadines,2006-02-15 04:44:00,Kingstown |
||||
Saudi Arabia,2006-02-15 04:44:00,"Abha, al-Hawiya, al-Qatif, Jedda, Tabuk" |
||||
Senegal,2006-02-15 04:44:00,Ziguinchor |
||||
Slovakia,2006-02-15 04:44:00,Bratislava |
||||
South Africa,2006-02-15 04:44:00,"Boksburg, Botshabelo, Chatsworth, Johannesburg, Kimberley, Klerksdorp, Newcastle, Paarl, Rustenburg, Soshanguve, Springs" |
||||
South Korea,2006-02-15 04:44:00,"Cheju, Kimchon, Naju, Tonghae, Uijongbu" |
||||
Spain,2006-02-15 04:44:00,"A Corua (La Corua), Donostia-San Sebastin, Gijn, Ourense (Orense), Santiago de Compostela" |
||||
Sri Lanka,2006-02-15 04:44:00,Jaffna |
||||
Sudan,2006-02-15 04:44:00,"al-Qadarif, Omdurman" |
||||
Sweden,2006-02-15 04:44:00,Malm |
||||
Switzerland,2006-02-15 04:44:00,"Basel, Bern, Lausanne" |
||||
Taiwan,2006-02-15 04:44:00,"Changhwa, Chiayi, Chungho, Fengshan, Hsichuh, Lungtan, Nantou, Tanshui, Touliu, Tsaotun" |
||||
Tanzania,2006-02-15 04:44:00,"Mwanza, Tabora, Zanzibar" |
||||
Thailand,2006-02-15 04:44:00,"Nakhon Sawan, Pak Kret, Songkhla" |
||||
Tonga,2006-02-15 04:44:00,Nukualofa |
||||
Tunisia,2006-02-15 04:44:00,Sousse |
||||
Turkey,2006-02-15 04:44:00,"Adana, Balikesir, Batman, Denizli, Eskisehir, Gaziantep, Inegl, Kilis, Ktahya, Osmaniye, Sivas, Sultanbeyli, Tarsus, Tokat, Usak" |
||||
Turkmenistan,2006-02-15 04:44:00,Ashgabat |
||||
Tuvalu,2006-02-15 04:44:00,Funafuti |
||||
Ukraine,2006-02-15 04:44:00,"Kamjanets-Podilskyi, Konotop, Mukateve, ostka, Simferopol, Sumy" |
||||
United Arab Emirates,2006-02-15 04:44:00,"Abu Dhabi, al-Ayn, Sharja" |
||||
United Kingdom,2006-02-15 04:44:00,"Bradford, Dundee, London, Southampton, Southend-on-Sea, Southport, Stockport, York" |
||||
United States,2006-02-15 04:44:00,"Akron, Arlington, Augusta-Richmond County, Aurora, Bellevue, Brockton, Cape Coral, Citrus Heights, Clarksville, Compton, Dallas, Dayton, El Monte, Fontana, Garden Grove, Garland, Grand Prairie, Greensboro, Joliet, Kansas City, Lancaster, Laredo, Lincoln, Manchester, Memphis" |
||||
Venezuela,2006-02-15 04:44:00,"Barcelona, Caracas, Cuman, Maracabo, Ocumare del Tuy, Valencia, Valle de la Pascua" |
||||
Vietnam,2006-02-15 04:44:00,"Cam Ranh, Haiphong, Hanoi, Nam Dinh, Nha Trang, Vinh" |
||||
"Virgin Islands, U.S.",2006-02-15 04:44:00,Charlotte Amalie |
||||
Yemen,2006-02-15 04:44:00,"Aden, Hodeida, Sanaa, Taizz" |
||||
Yugoslavia,2006-02-15 04:44:00,"Kragujevac, Novi Sad" |
||||
Zambia,2006-02-15 04:44:00,Kitwe |
@ -0,0 +1,4 @@
|
||||
Address,District,PostalCode,Phone,Location,Customer List,Staff List,City,Staff List1 |
||||
1661 Abha Drive,Tamil Nadu,14400,270456873752,"{""x"":78.8214191,""y"":10.3812871}",1,,Pudukkottai, |
||||
1993 Tabuk Lane,Tamil Nadu,64221,648482415405,"{""x"":80.1270701,""y"":12.9246028}",2,,Tambaram, |
||||
381 Kabul Way,Taipei,87272,55477302294,"{""x"":0,""y"":0}",2,,Hsichuh, |
@ -0,0 +1,5 @@
|
||||
{ |
||||
"country": "Afghanistan", |
||||
"city": ["Kabul"] |
||||
} |
||||
|
@ -0,0 +1,4 @@
|
||||
{ |
||||
"country": "Algeria", |
||||
"city": ["Batna", "Bchar", "Skikda"] |
||||
} |
@ -0,0 +1,4 @@
|
||||
{ |
||||
"country": "Americal Samoa", |
||||
"city": ["Tafuna"] |
||||
} |
@ -0,0 +1,4 @@
|
||||
{ |
||||
"country": "Angola", |
||||
"city": ["Benguela", "Namibe"] |
||||
} |
@ -0,0 +1,4 @@
|
||||
{ |
||||
"country": "Anguilla", |
||||
"city": ["South Hill"] |
||||
} |
@ -0,0 +1,4 @@
|
||||
{ |
||||
"country": "Argentina", |
||||
"city": ["Almirante Brown", "Avellaneda", "Beha Blanca", "Crdoba"] |
||||
} |
Binary file not shown.
@ -0,0 +1,22 @@
|
||||
import { test } from '@playwright/test'; |
||||
import { DashboardPage } from '../pages/Dashboard'; |
||||
import setup from '../setup'; |
||||
import { ToolbarPage } from '../pages/Dashboard/common/Toolbar'; |
||||
|
||||
test.describe.only('Test block name', () => { |
||||
let dashboard: DashboardPage; |
||||
let toolbar: ToolbarPage; |
||||
let context: any; |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
context = await setup({ page }); |
||||
dashboard = new DashboardPage(page, context.project); |
||||
toolbar = dashboard.grid.toolbar; |
||||
}); |
||||
|
||||
test('Test case name', async () => { |
||||
// close 'Team & Auth' tab
|
||||
await dashboard.closeTab({ title: 'Team & Auth' }); |
||||
await dashboard.treeView.openTable({ title: 'Country' }); |
||||
}); |
||||
}); |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@
|
||||
{ |
||||
"name": "playwright", |
||||
"version": "1.0.0", |
||||
"description": "", |
||||
"main": "index.js", |
||||
"scripts": { |
||||
"test": "TRACE=true npx playwright test --workers=4", |
||||
"test:repeat": "TRACE=true npx playwright test --workers=4 --repeat-each=10", |
||||
"test:quick": "TRACE=true PW_QUICK_TEST=1 npx playwright test --workers=4", |
||||
"test:debug": "./startPlayWrightServer.sh; PW_TEST_REUSE_CONTEXT=1 PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:31000/ PWDEBUG=console npx playwright test -c playwright.config.ts --headed --project=chromium --retries 0 --timeout 0 --workers 1 --max-failures=1", |
||||
"test:debug:quick:sqlite": "./startPlayWrightServer.sh; PW_QUICK_TEST=1 PW_TEST_REUSE_CONTEXT=1 PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:31000/ PWDEBUG=console npx playwright test -c playwright.config.ts --headed --project=chromium --retries 0 --timeout 5 --workers 1 --max-failures=1", |
||||
"ci:test:mysql": "E2E_DB_TYPE=mysql npx playwright test --workers=2" |
||||
}, |
||||
"keywords": [], |
||||
"author": "", |
||||
"license": "ISC", |
||||
"devDependencies": { |
||||
"@playwright/test": "1.27.1", |
||||
"@typescript-eslint/eslint-plugin": "^4.0.1", |
||||
"@typescript-eslint/parser": "^4.0.1", |
||||
"axios": "^0.24.0", |
||||
"dotenv": "^16.0.3", |
||||
"eslint": "^7.8.0", |
||||
"eslint-config-prettier": "^6.15.0", |
||||
"eslint-plugin-eslint-comments": "^3.2.0", |
||||
"eslint-plugin-functional": "^3.0.2", |
||||
"eslint-plugin-import": "^2.22.0", |
||||
"eslint-plugin-prettier": "^4.0.0", |
||||
"eslint-plugin-json": "^3.1.0", |
||||
"husky": "^8.0.1", |
||||
"lint-staged": "^13.0.3", |
||||
"mysql2": "^2.3.3", |
||||
"prettier": "^2.7.1", |
||||
"promised-sqlite3": "^1.2.0" |
||||
}, |
||||
"dependencies": { |
||||
"body-parser": "^1.20.1", |
||||
"express": "^4.18.2", |
||||
"xlsx": "^0.18.5" |
||||
} |
||||
} |
@ -0,0 +1,81 @@
|
||||
import { Locator, Page } from '@playwright/test'; |
||||
|
||||
type ResponseSelector = (json: any) => boolean; |
||||
|
||||
export default abstract class BasePage { |
||||
readonly rootPage: Page; |
||||
|
||||
abstract get(args?: any): Locator; |
||||
|
||||
constructor(rootPage: Page) { |
||||
this.rootPage = rootPage; |
||||
} |
||||
|
||||
async verifyToast({ message }: { message: string }) { |
||||
await this.rootPage.locator('.ant-message .ant-message-notice-content', { hasText: message }).last().isVisible(); |
||||
} |
||||
|
||||
async waitForResponse({ |
||||
uiAction, |
||||
httpMethodsToMatch = [], |
||||
requestUrlPathToMatch, |
||||
responseJsonMatcher, |
||||
}: { |
||||
uiAction: Promise<any>; |
||||
requestUrlPathToMatch: string; |
||||
httpMethodsToMatch?: string[]; |
||||
responseJsonMatcher?: ResponseSelector; |
||||
}) { |
||||
await Promise.all([ |
||||
this.rootPage.waitForResponse(async res => { |
||||
let isResJsonMatched = true; |
||||
if (responseJsonMatcher) { |
||||
try { |
||||
isResJsonMatched = responseJsonMatcher(await res.json()); |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
res.request().url().includes(requestUrlPathToMatch) && |
||||
httpMethodsToMatch.includes(res.request().method()) && |
||||
isResJsonMatched |
||||
); |
||||
}), |
||||
uiAction, |
||||
]); |
||||
} |
||||
|
||||
async attachFile({ filePickUIAction, filePath }: { filePickUIAction: Promise<any>; filePath: string }) { |
||||
const [fileChooser] = await Promise.all([ |
||||
// It is important to call waitForEvent before click to set up waiting.
|
||||
this.rootPage.waitForEvent('filechooser'), |
||||
// Opens the file chooser.
|
||||
filePickUIAction, |
||||
]); |
||||
await fileChooser.setFiles(filePath); |
||||
} |
||||
|
||||
async downloadAndGetFile({ downloadUIAction }: { downloadUIAction: Promise<any> }) { |
||||
const [download] = await Promise.all([ |
||||
// It is important to call waitForEvent before click to set up waiting.
|
||||
this.rootPage.waitForEvent('download'), |
||||
// Triggers the download.
|
||||
downloadUIAction, |
||||
]); |
||||
// wait for download to complete
|
||||
if (await download.failure()) { |
||||
throw new Error('Download failed'); |
||||
} |
||||
|
||||
const file = await download.createReadStream(); |
||||
const data = await new Promise((resolve, reject) => { |
||||
let data = ''; |
||||
file?.on('data', chunk => (data += chunk)); |
||||
file?.on('end', () => resolve(data)); |
||||
file?.on('error', reject); |
||||
}); |
||||
return data as any; |
||||
} |
||||
} |
@ -0,0 +1,112 @@
|
||||
import { expect, Locator } from '@playwright/test'; |
||||
import BasePage from '../../Base'; |
||||
import { DashboardPage } from '..'; |
||||
|
||||
export class ExpandedFormPage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly addNewTableButton: Locator; |
||||
readonly copyUrlButton: Locator; |
||||
readonly toggleCommentsButton: Locator; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
this.addNewTableButton = this.dashboard.get().locator('.nc-add-new-table'); |
||||
this.copyUrlButton = this.dashboard.get().locator('.nc-copy-row-url:visible'); |
||||
this.toggleCommentsButton = this.dashboard.get().locator('.nc-toggle-comments:visible'); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator(`.nc-drawer-expanded-form`); |
||||
} |
||||
|
||||
async gotoUsingUrlAndRowId({ rowId }: { rowId: string }) { |
||||
const url = await this.dashboard.rootPage.url(); |
||||
const expandedFormUrl = '/' + url.split('/').slice(3).join('/').split('?')[0] + `?rowId=${rowId}`; |
||||
await this.rootPage.goto(expandedFormUrl); |
||||
await this.dashboard.waitForLoaderToDisappear(); |
||||
} |
||||
|
||||
async fillField({ columnTitle, value, type = 'text' }: { columnTitle: string; value: string; type?: string }) { |
||||
const field = this.get().locator(`[data-nc="nc-expand-col-${columnTitle}"]`); |
||||
await field.hover(); |
||||
switch (type) { |
||||
case 'text': |
||||
await field.locator('input').fill(value); |
||||
break; |
||||
case 'belongsTo': |
||||
await field.locator('.nc-action-icon').click(); |
||||
await this.dashboard.linkRecord.select(value); |
||||
break; |
||||
case 'hasMany': |
||||
case 'manyToMany': |
||||
await field.locator(`[data-cy="nc-child-list-button-link-to"]`).click(); |
||||
await this.dashboard.linkRecord.select(value); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
async save({ |
||||
waitForRowsData = true, |
||||
}: { |
||||
waitForRowsData?: boolean; |
||||
} = {}) { |
||||
const saveRowAction = this.get().locator('button:has-text("Save Row")').click(); |
||||
if (waitForRowsData) { |
||||
await this.waitForResponse({ |
||||
uiAction: saveRowAction, |
||||
requestUrlPathToMatch: 'api/v1/db/data/noco/', |
||||
httpMethodsToMatch: ['GET'], |
||||
responseJsonMatcher: json => json['pageInfo'], |
||||
}); |
||||
} else { |
||||
await this.waitForResponse({ |
||||
uiAction: saveRowAction, |
||||
requestUrlPathToMatch: 'api/v1/db/data/noco/', |
||||
httpMethodsToMatch: ['POST'], |
||||
}); |
||||
} |
||||
|
||||
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' }); |
||||
} |
||||
|
||||
async verify({ header, url }: { header: string; url: string }) { |
||||
await expect(this.get().locator(`.nc-expanded-form-header`).last()).toContainText(header); |
||||
await expect.poll(() => this.rootPage.url()).toContain(url); |
||||
} |
||||
|
||||
async close() { |
||||
await this.rootPage.keyboard.press('Escape'); |
||||
} |
||||
|
||||
async cancel() { |
||||
await this.get().locator('button:has-text("Cancel")').last().click(); |
||||
} |
||||
|
||||
async openChildCard(param: { column: string; title: string }) { |
||||
const childList = await this.get().locator(`[data-nc="nc-expand-col-${param.column}"]`); |
||||
await childList.locator(`.ant-card:has-text("${param.title}")`).click(); |
||||
} |
||||
|
||||
async verifyCount({ count }: { count: number }) { |
||||
return await expect(this.rootPage.locator(`.nc-drawer-expanded-form .ant-drawer-content`)).toHaveCount(count); |
||||
} |
||||
|
||||
async validateRoleAccess(param: { role: string }) { |
||||
if (param.role === 'commenter' || param.role === 'viewer') { |
||||
await expect(await this.get().locator('button:has-text("Save Row")')).toBeDisabled(); |
||||
} else { |
||||
await expect(await this.get().locator('button:has-text("Save Row")')).toBeEnabled(); |
||||
} |
||||
if (param.role === 'viewer') { |
||||
await expect(await this.toggleCommentsButton).toHaveCount(0); |
||||
} else { |
||||
await expect(await this.toggleCommentsButton).toHaveCount(1); |
||||
} |
||||
// press escape to close the expanded form
|
||||
await this.rootPage.keyboard.press('Escape'); |
||||
} |
||||
} |
@ -0,0 +1,252 @@
|
||||
import { expect, Locator } from '@playwright/test'; |
||||
import { DashboardPage } from '..'; |
||||
import BasePage from '../../Base'; |
||||
import { ToolbarPage } from '../common/Toolbar'; |
||||
|
||||
export class FormPage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly toolbar: ToolbarPage; |
||||
|
||||
readonly addAllButton: Locator; |
||||
readonly removeAllButton: Locator; |
||||
readonly submitButton: Locator; |
||||
|
||||
readonly showAnotherFormRadioButton: Locator; |
||||
readonly showAnotherFormAfter5SecRadioButton: Locator; |
||||
readonly emailMeRadioButton: Locator; |
||||
|
||||
readonly formHeading: Locator; |
||||
readonly formSubHeading: Locator; |
||||
readonly afterSubmitMsg: Locator; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
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.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"]'); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator('[data-nc="nc-form-wrapper"]'); |
||||
} |
||||
|
||||
getFormAfterSubmit() { |
||||
return this.dashboard.get().locator('[data-nc="nc-form-wrapper-submit"]'); |
||||
} |
||||
|
||||
getFormHiddenColumn() { |
||||
return this.get().locator('[data-nc="nc-form-hidden-column"]'); |
||||
} |
||||
|
||||
getFormFields() { |
||||
return this.get().locator('[data-nc="nc-form-fields"]'); |
||||
} |
||||
|
||||
getDragNDropToHide() { |
||||
return this.get().locator('[data-nc="nc-drag-n-drop-to-hide"]'); |
||||
} |
||||
|
||||
getFormFieldsRemoveIcon() { |
||||
return this.get().locator('[data-nc="nc-field-remove-icon"]'); |
||||
} |
||||
|
||||
getFormFieldsRequired() { |
||||
return this.get().locator('[data-nc="nc-form-input-required"]'); |
||||
} |
||||
|
||||
getFormFieldsInputLabel() { |
||||
return this.get().locator('input[data-nc="nc-form-input-label"]:visible'); |
||||
} |
||||
|
||||
getFormFieldsInputHelpText() { |
||||
return this.get().locator('input[data-nc="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); |
||||
} |
||||
|
||||
async verifyFormFieldHelpText({ index, helpText }: { index: number; helpText: string }) { |
||||
await expect( |
||||
await this.getFormFields().nth(index).locator('[data-nc="nc-form-input-help-text-label"]') |
||||
).toContainText(helpText); |
||||
} |
||||
|
||||
async verifyFieldsIsEditable({ index }: { index: number }) { |
||||
await expect(await this.getFormFields().nth(index)).toHaveClass(/nc-editable/); |
||||
} |
||||
|
||||
async verifyAfterSubmitMsg({ msg }: { msg: string }) { |
||||
await expect((await this.afterSubmitMsg.inputValue()).includes(msg)).toBeTruthy(); |
||||
} |
||||
|
||||
async verifyFormViewFieldsOrder({ fields }: { fields: string[] }) { |
||||
const fieldLabels = await this.get().locator('[data-nc="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]); |
||||
} |
||||
} |
||||
|
||||
async reorderFields({ sourceField, destinationField }: { sourceField: string; destinationField: string }) { |
||||
await expect(await this.get().locator(`.nc-form-drag-${sourceField}`)).toBeVisible(); |
||||
await expect(await this.get().locator(`.nc-form-drag-${destinationField}`)).toBeVisible(); |
||||
const src = await this.get().locator(`.nc-form-drag-${sourceField.replace(' ', '')}`); |
||||
const dst = await this.get().locator(`.nc-form-drag-${destinationField.replace(' ', '')}`); |
||||
await src.dragTo(dst); |
||||
} |
||||
|
||||
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"]`); |
||||
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(); |
||||
} |
||||
} |
||||
|
||||
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 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}"]`); |
||||
await src.click(); |
||||
} |
||||
} |
||||
|
||||
async removeAllFields() { |
||||
await this.removeAllButton.click(); |
||||
} |
||||
|
||||
async addAllFields() { |
||||
await this.addAllButton.click(); |
||||
} |
||||
|
||||
async configureHeader(param: { subtitle: string; title: string }) { |
||||
await this.formHeading.fill(param.title); |
||||
await this.formSubHeading.fill(param.subtitle); |
||||
} |
||||
|
||||
async verifyHeader(param: { subtitle: string; title: string }) { |
||||
await expect.poll(async () => await this.formHeading.inputValue()).toBe(param.title); |
||||
await expect.poll(async () => await this.formSubHeading.inputValue()).toBe(param.subtitle); |
||||
} |
||||
|
||||
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`) |
||||
.fill(param[i].value); |
||||
} |
||||
} |
||||
|
||||
async configureField({ |
||||
field, |
||||
required, |
||||
label, |
||||
helpText, |
||||
}: { |
||||
field: string; |
||||
required: boolean; |
||||
label: string; |
||||
helpText: string; |
||||
}) { |
||||
await this.get() |
||||
.locator(`.nc-form-drag-${field.replace(' ', '')}`) |
||||
.locator('div[data-nc="nc-form-input-label"]') |
||||
.click(); |
||||
await this.getFormFieldsInputLabel().fill(label); |
||||
await this.getFormFieldsInputHelpText().fill(helpText); |
||||
if (required) { |
||||
await this.get() |
||||
.locator(`.nc-form-drag-${field.replace(' ', '')}`) |
||||
.click(); |
||||
} |
||||
await this.formHeading.click(); |
||||
} |
||||
|
||||
async verifyField({ |
||||
field, |
||||
required, |
||||
label, |
||||
helpText, |
||||
}: { |
||||
field: string; |
||||
required: boolean; |
||||
label: string; |
||||
helpText: string; |
||||
}) { |
||||
let expectText = ''; |
||||
if (required) expectText = label + ' *'; |
||||
else expectText = label; |
||||
|
||||
const fieldLabel = await this.get() |
||||
.locator(`.nc-form-drag-${field.replace(' ', '')}`) |
||||
.locator('div[data-nc="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"]'); |
||||
await expect(fieldHelpText).toHaveText(helpText); |
||||
} |
||||
|
||||
async submitForm() { |
||||
await this.submitButton.click(); |
||||
} |
||||
|
||||
async verifyStatePostSubmit(param: { message?: string; submitAnotherForm?: boolean; showBlankForm?: boolean }) { |
||||
if (undefined !== param.message) { |
||||
await expect(await this.getFormAfterSubmit()).toContainText(param.message); |
||||
} |
||||
if (true === param.submitAnotherForm) { |
||||
await expect(await this.getFormAfterSubmit().locator('button:has-text("Submit Another Form")')).toBeVisible(); |
||||
} |
||||
if (true === param.showBlankForm) { |
||||
await this.get().waitFor(); |
||||
} |
||||
} |
||||
|
||||
async configureSubmitMessage(param: { message: string }) { |
||||
await this.afterSubmitMsg.fill(param.message); |
||||
} |
||||
|
||||
submitAnotherForm() { |
||||
return this.getFormAfterSubmit().locator('button:has-text("Submit Another Form")'); |
||||
} |
||||
|
||||
// todo: Wait for render to complete
|
||||
async waitLoading() { |
||||
await this.rootPage.waitForTimeout(1000); |
||||
} |
||||
|
||||
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"]') |
||||
).toBeVisible(); |
||||
} |
||||
if (true === param.submitAnotherForm) { |
||||
await expect( |
||||
this.get().locator('[data-nc="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(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,32 @@
|
||||
import { DashboardPage } from '..'; |
||||
import BasePage from '../../Base'; |
||||
import { ToolbarPage } from '../common/Toolbar'; |
||||
|
||||
export class GalleryPage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly toolbar: ToolbarPage; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
this.toolbar = new ToolbarPage(this); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator('[data-nc="nc-gallery-wrapper"]'); |
||||
} |
||||
|
||||
card(index: number) { |
||||
return this.get().locator(`.ant-card`).nth(index); |
||||
} |
||||
|
||||
async openExpandedRow({ index }: { index: number }) { |
||||
await this.card(index).click(); |
||||
await (await this.rootPage.locator('.ant-drawer-body').elementHandle())?.waitForElementState('stable'); |
||||
} |
||||
|
||||
// todo: Wait for render to complete
|
||||
async waitLoading() { |
||||
await this.rootPage.waitForTimeout(1000); |
||||
} |
||||
} |
@ -0,0 +1,56 @@
|
||||
import BasePage from '../../../../Base'; |
||||
import { DashboardPage } from '../../../index'; |
||||
import { expect } from '@playwright/test'; |
||||
|
||||
export class ChildList extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator(`.nc-modal-child-list`); |
||||
} |
||||
|
||||
async verify({ cardTitle, linkField }: { cardTitle: string[]; linkField: string }) { |
||||
// DOM element validation
|
||||
// title: Child list
|
||||
// button: Link to 'City'
|
||||
// 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(); |
||||
|
||||
// child list body validation (card count, card title)
|
||||
const cardCount = cardTitle.length; |
||||
await this.get().locator('.ant-modal-content').waitFor(); |
||||
{ |
||||
const childList = this.get().locator(`.ant-card`); |
||||
const childCards = await childList.count(); |
||||
await expect(childCards).toEqual(cardCount); |
||||
for (let i = 0; i < cardCount; i++) { |
||||
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(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async close() { |
||||
await this.get().locator(`.ant-modal-close-x`).click(); |
||||
await this.get().waitFor({ state: 'hidden' }); |
||||
} |
||||
|
||||
async openLinkRecord({ linkTableTitle }: { linkTableTitle: string }) { |
||||
const openActions = this.get().locator(`button:has-text("Link to '${linkTableTitle}'")`).click(); |
||||
await this.waitForResponse({ |
||||
requestUrlPathToMatch: '/exclude', |
||||
httpMethodsToMatch: ['GET'], |
||||
uiAction: openActions, |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,49 @@
|
||||
import BasePage from '../../../../Base'; |
||||
import { DashboardPage } from '../../../index'; |
||||
import { expect } from '@playwright/test'; |
||||
|
||||
export class LinkRecord extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
} |
||||
|
||||
async verify(cardTitle?: string[]) { |
||||
await this.dashboard.get().locator('.nc-modal-link-record').waitFor(); |
||||
const linkRecord = await this.get(); |
||||
|
||||
// DOM element validation
|
||||
// title: Link Record
|
||||
// button: Add new record
|
||||
// icon: reload
|
||||
await expect(this.get().locator(`.ant-modal-title`)).toHaveText(`Link record`); |
||||
await expect(await linkRecord.locator(`button:has-text("Add new record")`).isVisible()).toBeTruthy(); |
||||
await expect(await linkRecord.locator(`.nc-reload`).isVisible()).toBeTruthy(); |
||||
// placeholder: Filter query
|
||||
await expect(await linkRecord.locator(`[placeholder="Filter query"]`).isVisible()).toBeTruthy(); |
||||
|
||||
{ |
||||
const childList = linkRecord.locator(`.ant-card`); |
||||
const childCards = await childList.count(); |
||||
await expect(childCards).toEqual(cardTitle.length); |
||||
for (let i = 0; i < cardTitle.length; i++) { |
||||
await expect(await childList.nth(i).textContent()).toContain(cardTitle[i]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async select(cardTitle: string) { |
||||
await this.get().locator(`.ant-card:has-text("${cardTitle}"):visible`).click(); |
||||
} |
||||
|
||||
async close() { |
||||
await this.get().locator(`.ant-modal-close-x`).click(); |
||||
await this.get().waitFor({ state: 'hidden' }); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator(`.nc-modal-link-record`); |
||||
} |
||||
} |
@ -0,0 +1,78 @@
|
||||
import { ColumnPageObject } from '.'; |
||||
import BasePage from '../../../Base'; |
||||
|
||||
export class SelectOptionColumnPageObject extends BasePage { |
||||
readonly column: ColumnPageObject; |
||||
|
||||
constructor(column: ColumnPageObject) { |
||||
super(column.rootPage); |
||||
this.column = column; |
||||
} |
||||
|
||||
get() { |
||||
return this.column.get(); |
||||
} |
||||
|
||||
async addOption({ |
||||
index, |
||||
columnTitle, |
||||
option, |
||||
skipColumnModal, |
||||
}: { |
||||
index: number; |
||||
option: string; |
||||
skipColumnModal?: boolean; |
||||
columnTitle?: string; |
||||
}) { |
||||
if (!skipColumnModal && columnTitle) await this.column.openEdit({ title: columnTitle }); |
||||
|
||||
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); |
||||
|
||||
if (!skipColumnModal && columnTitle) await this.column.save({ isUpdated: true }); |
||||
} |
||||
|
||||
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.save({ isUpdated: true }); |
||||
} |
||||
|
||||
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.save({ isUpdated: true }); |
||||
} |
||||
|
||||
async reorderOption({ |
||||
columnTitle, |
||||
sourceOption, |
||||
destinationOption, |
||||
}: { |
||||
columnTitle: string; |
||||
sourceOption: string; |
||||
destinationOption: string; |
||||
}) { |
||||
await this.column.openEdit({ title: columnTitle }); |
||||
|
||||
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}"]`, |
||||
{ |
||||
force: true, |
||||
} |
||||
); |
||||
|
||||
await this.column.save({ isUpdated: true }); |
||||
} |
||||
} |
@ -0,0 +1,219 @@
|
||||
import { expect, Page } from '@playwright/test'; |
||||
import { GridPage } from '..'; |
||||
import BasePage from '../../../Base'; |
||||
import { SelectOptionColumnPageObject } from './SelectOptionColumn'; |
||||
|
||||
export class ColumnPageObject extends BasePage { |
||||
readonly grid: GridPage; |
||||
readonly selectOption: SelectOptionColumnPageObject; |
||||
|
||||
constructor(grid: GridPage) { |
||||
super(grid.rootPage); |
||||
this.grid = grid; |
||||
this.selectOption = new SelectOptionColumnPageObject(this); |
||||
} |
||||
|
||||
get() { |
||||
return this.rootPage.locator('form[data-nc="add-or-edit-column"]'); |
||||
} |
||||
|
||||
async create({ |
||||
title, |
||||
type = 'SingleLineText', |
||||
formula = '', |
||||
childTable = '', |
||||
childColumn = '', |
||||
relationType = '', |
||||
rollupType = '', |
||||
format = '', |
||||
}: { |
||||
title: string; |
||||
type?: string; |
||||
formula?: string; |
||||
childTable?: string; |
||||
childColumn?: string; |
||||
relationType?: string; |
||||
rollupType?: string; |
||||
format?: string; |
||||
}) { |
||||
await this.grid.get().locator('.nc-column-add').click(); |
||||
await this.rootPage.waitForTimeout(500); |
||||
await this.fillTitle({ title }); |
||||
await this.rootPage.waitForTimeout(500); |
||||
await this.selectType({ type }); |
||||
await this.rootPage.waitForTimeout(500); |
||||
|
||||
switch (type) { |
||||
case 'SingleTextLine': |
||||
break; |
||||
case 'SingleSelect': |
||||
case 'MultiSelect': |
||||
await this.selectOption.addOption({ |
||||
index: 0, |
||||
option: 'Option 1', |
||||
skipColumnModal: true, |
||||
}); |
||||
await this.selectOption.addOption({ |
||||
index: 1, |
||||
option: 'Option 2', |
||||
skipColumnModal: true, |
||||
}); |
||||
break; |
||||
case 'Duration': |
||||
await this.get().locator('.ant-select-single').nth(1).click(); |
||||
await this.rootPage |
||||
.locator(`.ant-select-item`, { |
||||
hasText: format, |
||||
}) |
||||
.click(); |
||||
break; |
||||
case 'Formula': |
||||
await this.get().locator('.nc-formula-input').fill(formula); |
||||
break; |
||||
case 'Lookup': |
||||
await this.get().locator('.ant-select-single').nth(1).click(); |
||||
await this.rootPage |
||||
.locator(`.ant-select-item`, { |
||||
hasText: childTable, |
||||
}) |
||||
.click(); |
||||
await this.get().locator('.ant-select-single').nth(2).click(); |
||||
await this.rootPage |
||||
.locator(`.ant-select-item`, { |
||||
hasText: childColumn, |
||||
}) |
||||
.click(); |
||||
break; |
||||
case 'Rollup': |
||||
await this.get().locator('.ant-select-single').nth(1).click(); |
||||
await this.rootPage |
||||
.locator(`.ant-select-item`, { |
||||
hasText: childTable, |
||||
}) |
||||
.click(); |
||||
await this.get().locator('.ant-select-single').nth(2).click(); |
||||
await this.rootPage |
||||
.locator(`.nc-dropdown-relation-column >> .ant-select-item`, { |
||||
hasText: childColumn, |
||||
}) |
||||
.click(); |
||||
await this.get().locator('.ant-select-single').nth(3).click(); |
||||
await this.rootPage |
||||
.locator(`.nc-dropdown-rollup-function >> .ant-select-item`, { |
||||
hasText: rollupType, |
||||
}) |
||||
.nth(0) |
||||
.click(); |
||||
break; |
||||
case 'LinkToAnotherRecord': |
||||
await this.get() |
||||
.locator('.nc-ltar-relation-type >> .ant-radio') |
||||
.nth(relationType === 'Has Many' ? 0 : 1) |
||||
.click(); |
||||
await this.get().locator('.ant-select-single').nth(1).click(); |
||||
await this.rootPage.locator(`.nc-ltar-child-table >> input[type="search"]`).fill(childTable); |
||||
await this.rootPage |
||||
.locator(`.nc-dropdown-ltar-child-table >> .ant-select-item`, { |
||||
hasText: childTable, |
||||
}) |
||||
.nth(0) |
||||
.click(); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
|
||||
await this.save(); |
||||
} |
||||
|
||||
async fillTitle({ title }: { title: string }) { |
||||
await this.get().locator('.nc-column-name-input').fill(title); |
||||
} |
||||
|
||||
async selectType({ type }: { type: string }) { |
||||
await this.get().locator('.ant-select-selector > .ant-select-selection-item').click(); |
||||
|
||||
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').waitFor(); |
||||
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').fill(type); |
||||
|
||||
// Select column type
|
||||
await this.rootPage.locator(`text=${type}`).nth(1).click(); |
||||
} |
||||
|
||||
async delete({ title }: { title: string }) { |
||||
await this.grid.get().locator(`th[data-title="${title}"] >> svg.ant-dropdown-trigger`).click(); |
||||
// await this.rootPage.locator('li[role="menuitem"]:has-text("Delete")').waitFor();
|
||||
await this.rootPage.locator('li[role="menuitem"]:has-text("Delete")').click(); |
||||
|
||||
await this.rootPage.locator('button:has-text("Delete")').click(); |
||||
|
||||
// wait till modal is closed
|
||||
await this.rootPage.locator('.nc-modal-column-delete').waitFor({ state: 'hidden' }); |
||||
} |
||||
|
||||
async openEdit({ |
||||
title, |
||||
type = 'SingleLineText', |
||||
formula = '', |
||||
format, |
||||
}: { |
||||
title: string; |
||||
type?: string; |
||||
formula?: string; |
||||
format?: string; |
||||
}) { |
||||
await this.grid.get().locator(`th[data-title="${title}"] .nc-ui-dt-dropdown`).click(); |
||||
await this.rootPage.locator('li[role="menuitem"]:has-text("Edit")').click(); |
||||
|
||||
await this.get().waitFor({ state: 'visible' }); |
||||
|
||||
switch (type) { |
||||
case 'Formula': |
||||
await this.get().locator('.nc-formula-input').fill(formula); |
||||
break; |
||||
case 'Duration': |
||||
await this.get().locator('.ant-select-single').nth(1).click(); |
||||
await this.rootPage |
||||
.locator(`.ant-select-item`, { |
||||
hasText: format, |
||||
}) |
||||
.click(); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
} |
||||
|
||||
async save({ isUpdated }: { isUpdated?: boolean } = {}) { |
||||
await this.waitForResponse({ |
||||
uiAction: this.get().locator('button:has-text("Save")').click(), |
||||
requestUrlPathToMatch: 'api/v1/db/data/noco/', |
||||
httpMethodsToMatch: ['GET'], |
||||
responseJsonMatcher: json => json['pageInfo'], |
||||
}); |
||||
|
||||
await this.verifyToast({ |
||||
message: isUpdated ? 'Column updated' : 'Column created', |
||||
}); |
||||
await this.get().waitFor({ state: 'hidden' }); |
||||
await this.rootPage.waitForTimeout(200); |
||||
} |
||||
|
||||
async verify({ title, isVisible = true }: { title: string; isVisible?: boolean }) { |
||||
if (!isVisible) { |
||||
return await expect(await this.rootPage.locator(`th[data-title="${title}"]`)).not.toBeVisible(); |
||||
} |
||||
await await expect(this.rootPage.locator(`th[data-title="${title}"]`)).toContainText(title); |
||||
} |
||||
|
||||
async verifyRoleAccess(param: { role: string }) { |
||||
await expect(this.grid.get().locator('.nc-column-add:visible')).toHaveCount(param.role === 'creator' ? 1 : 0); |
||||
await expect(this.grid.get().locator('.nc-ui-dt-dropdown:visible')).toHaveCount(param.role === 'creator' ? 3 : 0); |
||||
|
||||
if (param.role === 'creator') { |
||||
await this.grid.get().locator('.nc-ui-dt-dropdown:visible').first().click(); |
||||
await expect(this.rootPage.locator('.nc-dropdown-column-operations')).toHaveCount(1); |
||||
await this.grid.get().locator('.nc-ui-dt-dropdown:visible').first().click(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,280 @@
|
||||
import { expect, Locator } from '@playwright/test'; |
||||
import { DashboardPage } from '..'; |
||||
import BasePage from '../../Base'; |
||||
import { CellPageObject } from '../common/Cell'; |
||||
import { ColumnPageObject } from './Column'; |
||||
import { ToolbarPage } from '../common/Toolbar'; |
||||
import { ProjectMenuObject } from '../common/ProjectMenu'; |
||||
|
||||
export class GridPage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly addNewTableButton: Locator; |
||||
readonly dashboardPage: DashboardPage; |
||||
readonly column: ColumnPageObject; |
||||
readonly cell: CellPageObject; |
||||
readonly toolbar: ToolbarPage; |
||||
readonly projectMenu: ProjectMenuObject; |
||||
|
||||
constructor(dashboardPage: DashboardPage) { |
||||
super(dashboardPage.rootPage); |
||||
this.dashboard = dashboardPage; |
||||
this.addNewTableButton = dashboardPage.get().locator('.nc-add-new-table'); |
||||
this.column = new ColumnPageObject(this); |
||||
this.cell = new CellPageObject(this); |
||||
this.toolbar = new ToolbarPage(this); |
||||
this.projectMenu = new ProjectMenuObject(this); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator('[data-nc="nc-grid-wrapper"]'); |
||||
} |
||||
|
||||
row(index: number) { |
||||
return this.get().locator(`tr[data-nc="grid-row-${index}"]`); |
||||
} |
||||
|
||||
async rowCount() { |
||||
return await this.get().locator('.nc-grid-row').count(); |
||||
} |
||||
|
||||
async verifyRowCount({ count }: { count: number }) { |
||||
return await expect(this.get().locator('.nc-grid-row')).toHaveCount(count); |
||||
} |
||||
|
||||
private async _fillRow({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) { |
||||
const cell = this.cell.get({ index, columnHeader }); |
||||
await this.cell.dblclick({ |
||||
index, |
||||
columnHeader, |
||||
}); |
||||
|
||||
await cell.locator('input').fill(value); |
||||
} |
||||
|
||||
async addNewRow({ |
||||
index = 0, |
||||
columnHeader = 'Title', |
||||
value, |
||||
networkValidation = true, |
||||
}: { |
||||
index?: number; |
||||
columnHeader?: string; |
||||
value?: string; |
||||
networkValidation?: boolean; |
||||
} = {}) { |
||||
const rowValue = value ?? `Row ${index}`; |
||||
const rowCount = await this.get().locator('.nc-grid-row').count(); |
||||
await this.get().locator('.nc-grid-add-new-cell').click(); |
||||
|
||||
await expect(this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1); |
||||
|
||||
await this._fillRow({ index, columnHeader, value: rowValue }); |
||||
|
||||
const clickOnColumnHeaderToSave = this.get() |
||||
.locator(`[data-title="${columnHeader}"]`) |
||||
.locator(`span[title="${columnHeader}"]`) |
||||
.click(); |
||||
|
||||
if (networkValidation) { |
||||
await this.waitForResponse({ |
||||
uiAction: clickOnColumnHeaderToSave, |
||||
requestUrlPathToMatch: 'api/v1/db/data/noco', |
||||
httpMethodsToMatch: ['POST'], |
||||
responseJsonMatcher: resJson => resJson?.[columnHeader] === value, |
||||
}); |
||||
} else { |
||||
await this.rootPage.waitForTimeout(300); |
||||
} |
||||
|
||||
await this.dashboard.waitForLoaderToDisappear(); |
||||
} |
||||
|
||||
async editRow({ |
||||
index = 0, |
||||
columnHeader = 'Title', |
||||
value, |
||||
networkValidation = true, |
||||
}: { |
||||
index?: number; |
||||
columnHeader?: string; |
||||
value: string; |
||||
networkValidation?: boolean; |
||||
}) { |
||||
await this._fillRow({ index, columnHeader, value }); |
||||
|
||||
const clickOnColumnHeaderToSave = this.get() |
||||
.locator(`[data-title="${columnHeader}"]`) |
||||
.locator(`span[title="${columnHeader}"]`) |
||||
.click(); |
||||
|
||||
if (networkValidation) { |
||||
await this.waitForResponse({ |
||||
uiAction: clickOnColumnHeaderToSave, |
||||
requestUrlPathToMatch: 'api/v1/db/data/noco', |
||||
httpMethodsToMatch: ['PATCH'], |
||||
responseJsonMatcher: resJson => resJson?.[columnHeader] === value, |
||||
}); |
||||
} else { |
||||
await this.rootPage.waitForTimeout(300); |
||||
} |
||||
|
||||
await this.dashboard.waitForLoaderToDisappear(); |
||||
} |
||||
|
||||
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); |
||||
} |
||||
|
||||
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); |
||||
} |
||||
|
||||
async deleteRow(index: number) { |
||||
await this.get().locator(`td[data-nc="cell-Title-${index}"]`).click({ |
||||
button: 'right', |
||||
}); |
||||
|
||||
// Click text=Delete Row
|
||||
await this.rootPage.locator('text=Delete Row').click(); |
||||
// todo: improve selector
|
||||
await this.rootPage |
||||
.locator('span.ant-dropdown-menu-title-content > nc-project-menu-item') |
||||
.waitFor({ state: 'hidden' }); |
||||
|
||||
await this.rootPage.waitForTimeout(300); |
||||
await this.dashboard.waitForLoaderToDisappear(); |
||||
} |
||||
|
||||
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({ |
||||
button: 'right', |
||||
}); |
||||
// Click text=Insert New Row
|
||||
await this.rootPage.locator('text=Insert New Row').click(); |
||||
await expect(await this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1); |
||||
} |
||||
|
||||
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 (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-nc="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 this.rootPage.waitForTimeout(300); |
||||
} |
||||
|
||||
async deleteAll() { |
||||
await this.selectAll(); |
||||
await this.get().locator('[data-nc="nc-check-all"]').nth(0).click({ |
||||
button: 'right', |
||||
}); |
||||
await this.rootPage.locator('text=Delete Selected Rows').click(); |
||||
await this.dashboard.waitForLoaderToDisappear(); |
||||
} |
||||
|
||||
private async pagination({ page }: { page: string }) { |
||||
await this.get().locator(`.nc-pagination`).waitFor(); |
||||
|
||||
if (page === '<') return this.get().locator('.nc-pagination > .ant-pagination-prev'); |
||||
if (page === '>') return this.get().locator('.nc-pagination > .ant-pagination-next'); |
||||
|
||||
return this.get().locator(`.nc-pagination > .ant-pagination-item.ant-pagination-item-${page}`); |
||||
} |
||||
|
||||
async clickPagination({ page }: { page: string }) { |
||||
await this.waitForResponse({ |
||||
uiAction: (await this.pagination({ page })).click(), |
||||
httpMethodsToMatch: ['GET'], |
||||
requestUrlPathToMatch: '/views/', |
||||
responseJsonMatcher: resJson => resJson?.pageInfo, |
||||
}); |
||||
|
||||
await this.waitLoading(); |
||||
} |
||||
|
||||
async verifyActivePage({ page }: { page: string }) { |
||||
await expect(await this.pagination({ page })).toHaveClass(/ant-pagination-item-active/); |
||||
} |
||||
|
||||
async waitLoading() { |
||||
await this.dashboard.get().locator('[data-nc="grid-load-spinner"]').waitFor({ state: 'hidden' }); |
||||
} |
||||
|
||||
async verifyEditDisabled({ columnHeader = 'Title' }: { columnHeader?: string } = {}) { |
||||
// double click to toggle to edit mode
|
||||
const cell = this.cell.get({ index: 0, columnHeader: columnHeader }); |
||||
await this.cell.dblclick({ |
||||
index: 0, |
||||
columnHeader: columnHeader, |
||||
}); |
||||
await expect(await cell.locator('input')).not.toBeVisible(); |
||||
|
||||
// right click menu
|
||||
await this.get().locator(`td[data-nc="cell-${columnHeader}-0"]`).click({ |
||||
button: 'right', |
||||
}); |
||||
await expect(await this.rootPage.locator('text=Insert New Row')).not.toBeVisible(); |
||||
|
||||
// in cell-add
|
||||
await this.cell.get({ index: 0, columnHeader: 'City List' }).hover(); |
||||
await expect( |
||||
await this.cell.get({ index: 0, columnHeader: 'City List' }).locator('.nc-action-icon.nc-plus') |
||||
).not.toBeVisible(); |
||||
|
||||
// expand row
|
||||
await this.cell.get({ index: 0, columnHeader: 'City List' }).hover(); |
||||
await expect( |
||||
await this.cell.get({ index: 0, columnHeader: 'City List' }).locator('.nc-action-icon >> nth=0') |
||||
).not.toBeVisible(); |
||||
} |
||||
|
||||
async verifyEditEnabled({ columnHeader = 'Title' }: { columnHeader?: string } = {}) { |
||||
// double click to toggle to edit mode
|
||||
const cell = this.cell.get({ index: 0, columnHeader: columnHeader }); |
||||
await this.cell.dblclick({ |
||||
index: 0, |
||||
columnHeader: columnHeader, |
||||
}); |
||||
await expect(await cell.locator('input')).toBeVisible(); |
||||
|
||||
// right click menu
|
||||
await this.get().locator(`td[data-nc="cell-${columnHeader}-0"]`).click({ |
||||
button: 'right', |
||||
}); |
||||
await expect(await this.rootPage.locator('text=Insert New Row')).toBeVisible(); |
||||
|
||||
// in cell-add
|
||||
await this.cell.get({ index: 0, columnHeader: 'City List' }).hover(); |
||||
await expect( |
||||
await this.cell.get({ index: 0, columnHeader: 'City List' }).locator('.nc-action-icon.nc-plus') |
||||
).toBeVisible(); |
||||
|
||||
// expand row
|
||||
await this.cell.get({ index: 0, columnHeader: 'City List' }).hover(); |
||||
await expect( |
||||
await this.cell.get({ index: 0, columnHeader: 'City List' }).locator('.nc-action-icon.nc-arrow-expand') |
||||
).toBeVisible(); |
||||
} |
||||
|
||||
async validateRoleAccess(param: { role: string }) { |
||||
await this.column.verifyRoleAccess(param); |
||||
await this.cell.verifyRoleAccess(param); |
||||
await expect(this.get().locator('.nc-grid-add-new-cell')).toHaveCount( |
||||
param.role === 'creator' || param.role === 'editor' ? 1 : 0 |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,31 @@
|
||||
import { Locator } from '@playwright/test'; |
||||
import BasePage from '../../Base'; |
||||
import { DashboardPage } from '..'; |
||||
|
||||
export class ImportAirtablePage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly importButton: Locator; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
this.importButton = dashboard.get().locator('.nc-btn-airtable-import'); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator(`.nc-modal-airtable-import`); |
||||
} |
||||
|
||||
async import({ key, baseId }: { key: string; baseId: string }) { |
||||
// kludge: failing in headless mode
|
||||
// additional time to allow the modal to render completely
|
||||
await this.rootPage.waitForTimeout(1000); |
||||
|
||||
await this.get().locator(`.nc-input-api-key >> input`).fill(key); |
||||
await this.get().locator(`.nc-input-shared-base`).fill(baseId); |
||||
await this.importButton.click(); |
||||
|
||||
await this.get().locator(`button:has-text("Go to Dashboard")`).waitFor(); |
||||
await this.get().locator(`button:has-text("Go to Dashboard")`).click(); |
||||
} |
||||
} |
@ -0,0 +1,79 @@
|
||||
import { expect, Locator } from '@playwright/test'; |
||||
import BasePage from '../../Base'; |
||||
import { DashboardPage } from '..'; |
||||
|
||||
export class ImportTemplatePage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly importButton: Locator; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
this.importButton = dashboard.get().locator('.nc-btn-import'); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator(`.nc-modal-quick-import`); |
||||
} |
||||
|
||||
async getImportTableList() { |
||||
await this.get().locator(`.ant-collapse-header`).nth(0).waitFor(); |
||||
const tr = await this.get().locator(`.ant-collapse-header`); |
||||
const rowCount = await tr.count(); |
||||
const tableList: string[] = []; |
||||
for (let i = 0; i < rowCount; i++) { |
||||
const tableName = await tr.nth(i).innerText(); |
||||
tableList.push(tableName); |
||||
} |
||||
return tableList; |
||||
} |
||||
|
||||
async getImportColumnList() { |
||||
// return an array
|
||||
const columnList: { type: string; name: string }[] = []; |
||||
const tr = await this.get().locator(`tr.ant-table-row-level-0:visible`); |
||||
const rowCount = await tr.count(); |
||||
for (let i = 0; i < rowCount; i++) { |
||||
// replace \n and \t from innerText
|
||||
const columnType = await tr |
||||
.nth(i) |
||||
.innerText() |
||||
.then(text => text.replace(/\n|\t/g, '')); |
||||
const columnName = await tr.nth(i).locator(`input[type="text"]`).inputValue(); |
||||
columnList.push({ type: columnType, name: columnName }); |
||||
} |
||||
return columnList; |
||||
} |
||||
|
||||
// todo: Add polling logic to assertions
|
||||
async import({ file, result }: { file: string; result: any }) { |
||||
const importFile = this.get().locator(`input[type="file"]`); |
||||
await importFile.setInputFiles(file); |
||||
await this.importButton.click(); |
||||
|
||||
const tblList = await this.getImportTableList(); |
||||
for (let i = 0; i < result.length; i++) { |
||||
await expect(tblList[i]).toBe(result[i].name); |
||||
const columnList = await this.getImportColumnList(); |
||||
await expect(columnList).toEqual(result[i].columns); |
||||
if (i < result.length - 1) { |
||||
await this.expandTableList({ index: i + 1 }); |
||||
} |
||||
} |
||||
|
||||
await this.get().locator('button:has-text("Back"):visible').waitFor(); |
||||
await this.waitForResponse({ |
||||
requestUrlPathToMatch: '/api/v1/db/data/noco/', |
||||
httpMethodsToMatch: ['GET'], |
||||
uiAction: this.get().locator('button:has-text("Import"):visible').click(), |
||||
}); |
||||
await this.dashboard.waitForTabRender({ |
||||
title: tblList[0], |
||||
}); |
||||
} |
||||
|
||||
private async expandTableList(param: { index: number }) { |
||||
await this.get().locator(`.ant-collapse-header`).nth(param.index).click(); |
||||
await this.rootPage.waitForTimeout(1000); |
||||
} |
||||
} |
@ -0,0 +1,140 @@
|
||||
import { expect } from '@playwright/test'; |
||||
import { DashboardPage } from '..'; |
||||
import BasePage from '../../Base'; |
||||
import { ToolbarPage } from '../common/Toolbar'; |
||||
|
||||
export class KanbanPage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly toolbar: ToolbarPage; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
this.toolbar = new ToolbarPage(this); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator('[data-nc="nc-kanban-wrapper"]'); |
||||
} |
||||
|
||||
card(index: number) { |
||||
return this.get().locator(`.ant-card`).nth(index); |
||||
} |
||||
|
||||
async openExpandedRow({ index }: { index: number }) { |
||||
await this.card(index).click(); |
||||
await (await this.rootPage.locator('.ant-drawer-body').elementHandle())?.waitForElementState('stable'); |
||||
} |
||||
|
||||
// todo: Implement
|
||||
async addOption() {} |
||||
|
||||
// todo: Implement
|
||||
async dragDropCard(param: { from: string; to: string }) { |
||||
// const { from, to } = param;
|
||||
// const srcStack = await this.get().locator(`.nc-kanban-stack`).nth(1);
|
||||
// const dstStack = await this.get().locator(`.nc-kanban-stack`).nth(2);
|
||||
// const fromCard = await srcStack.locator(`.nc-kanban-item`).nth(1);
|
||||
// const toCard = await dstStack.locator(`.nc-kanban-item`).nth(1);
|
||||
// const [fromCard, toCard] = await Promise.all([
|
||||
// srcStack.locator(`.nc-kanban-item[data-draggable="true"]`).nth(0),
|
||||
// dstStack.locator(`.nc-kanban-item[data-draggable="true"]`).nth(0),
|
||||
// ]);
|
||||
// const fromCard = await this.get().locator(`.nc-kanban-item`).nth(0);
|
||||
// const toCard = await this.get().locator(`.nc-kanban-item`).nth(25);
|
||||
// await fromCard.dragTo(toCard);
|
||||
} |
||||
|
||||
async dragDropStack(param: { from: number; to: number }) { |
||||
const { from, to } = param; |
||||
const [fromStack, toStack] = await Promise.all([ |
||||
this.rootPage.locator(`.nc-kanban-stack-head`).nth(from), |
||||
this.rootPage.locator(`.nc-kanban-stack-head`).nth(to), |
||||
]); |
||||
await fromStack.dragTo(toStack); |
||||
} |
||||
|
||||
async verifyStackCount(param: { count: number }) { |
||||
const { count } = param; |
||||
await expect(this.get().locator(`.nc-kanban-stack`)).toHaveCount(count); |
||||
} |
||||
|
||||
async verifyStackOrder(param: { order: string[] }) { |
||||
const { order } = param; |
||||
const stacks = await this.get().locator(`.nc-kanban-stack`).count(); |
||||
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"]`); |
||||
await expect(stackTitle).toHaveText(order[i], { ignoreCase: true }); |
||||
} |
||||
} |
||||
|
||||
async verifyStackFooter(param: { count: number[] }) { |
||||
const { count } = param; |
||||
const stacks = await this.get().locator(`.nc-kanban-stack`).count(); |
||||
for (let i = 0; i < stacks; i++) { |
||||
const stack = await this.get().locator(`.nc-kanban-stack`).nth(i); |
||||
const stackFooter = await stack.locator(`.nc-kanban-data-count`).innerText(); |
||||
await expect(stackFooter).toContain(`${count[i]} record${count[i] !== 1 ? 's' : ''}`); |
||||
} |
||||
} |
||||
|
||||
async verifyCardCount(param: { count: number[] }) { |
||||
const { count } = param; |
||||
const stacks = await this.get().locator(`.nc-kanban-stack`).count(); |
||||
for (let i = 0; i < stacks; i++) { |
||||
const stack = await this.get().locator(`.nc-kanban-stack`).nth(i); |
||||
const stackCards = stack.locator(`.nc-kanban-item`); |
||||
await expect(stackCards).toHaveCount(count[i]); |
||||
} |
||||
} |
||||
|
||||
async verifyCardOrder(param: { order: string[]; stackIndex: number }) { |
||||
const { order, stackIndex } = param; |
||||
const stack = await this.get().locator(`.nc-kanban-stack`).nth(stackIndex); |
||||
for (let i = 0; i < order.length; i++) { |
||||
const card = await stack.locator(`.nc-kanban-item`).nth(i); |
||||
const cardTitle = await card.locator(`.nc-cell`); |
||||
await expect(cardTitle).toHaveText(order[i]); |
||||
} |
||||
} |
||||
|
||||
// todo: Wait for render to complete
|
||||
async waitLoading() { |
||||
await this.rootPage.waitForTimeout(1000); |
||||
} |
||||
|
||||
async addNewStack(param: { title: string }) { |
||||
await this.toolbar.clickAddEditStack(); |
||||
await this.toolbar.addEditStack.addOption({ title: param.title }); |
||||
} |
||||
|
||||
async collapseStack(param: { index: number }) { |
||||
await this.get().locator(`.nc-kanban-stack-head`).nth(param.index).click(); |
||||
const modal = await this.rootPage.locator(`.nc-dropdown-kanban-stack-context-menu`); |
||||
await modal.locator('.ant-dropdown-menu-item:has-text("Collapse Stack")').click(); |
||||
} |
||||
|
||||
async expandStack(param: { index: number }) { |
||||
await this.rootPage.locator(`.nc-kanban-collapsed-stack`).nth(param.index).click(); |
||||
} |
||||
|
||||
async verifyCollapseStackCount(param: { count: number }) { |
||||
await expect(this.rootPage.locator('.nc-kanban-collapsed-stack')).toHaveCount(param.count); |
||||
} |
||||
|
||||
async addCard(param: { stackIndex: number }) { |
||||
await this.get().locator(`.nc-kanban-stack-head`).nth(param.stackIndex).click(); |
||||
const modal = await this.rootPage.locator(`.nc-dropdown-kanban-stack-context-menu`); |
||||
await modal.locator('.ant-dropdown-menu-item:has-text("Add new record")').click(); |
||||
} |
||||
|
||||
async deleteStack(param: { index: number }) { |
||||
await this.get().locator(`.nc-kanban-stack-head`).nth(param.index).click(); |
||||
const modal = await this.rootPage.locator(`.nc-dropdown-kanban-stack-context-menu`); |
||||
await modal.locator('.ant-dropdown-menu-item:has-text("Delete Stack")').click(); |
||||
const confirmationModal = await this.rootPage.locator(`.nc-modal-kanban-delete-stack`); |
||||
await confirmationModal.locator(`button:has-text("Delete")`).click(); |
||||
} |
||||
} |
@ -0,0 +1,29 @@
|
||||
import { expect, Locator } from '@playwright/test'; |
||||
import { SettingsPage } from '.'; |
||||
import BasePage from '../../Base'; |
||||
|
||||
export class AclPage extends BasePage { |
||||
private readonly settings: SettingsPage; |
||||
|
||||
constructor(settings: SettingsPage) { |
||||
super(settings.rootPage); |
||||
this.settings = settings; |
||||
} |
||||
|
||||
get() { |
||||
return this.settings.get().locator(`[data-nc="nc-settings-subtab-UI Access Control"]`); |
||||
} |
||||
|
||||
async toggle({ table, role }: { table: string; role: string }) { |
||||
await this.get().locator(`.nc-acl-${table}-${role}-chkbox`).click(); |
||||
} |
||||
|
||||
async save() { |
||||
await this.waitForResponse({ |
||||
uiAction: this.get().locator(`button:has-text("Save")`).click(), |
||||
httpMethodsToMatch: ['POST'], |
||||
requestUrlPathToMatch: '/visibility-rules', |
||||
}); |
||||
await this.verifyToast({ message: 'Updated UI ACL for tables successfully' }); |
||||
} |
||||
} |
@ -0,0 +1,43 @@
|
||||
import { expect } from '@playwright/test'; |
||||
import { SettingsPage } from '.'; |
||||
import BasePage from '../../Base'; |
||||
|
||||
export class AppStoreSettingsPage extends BasePage { |
||||
private readonly settings: SettingsPage; |
||||
|
||||
constructor(settings: SettingsPage) { |
||||
super(settings.rootPage); |
||||
this.settings = settings; |
||||
} |
||||
|
||||
get() { |
||||
return this.settings.get().locator(`[data-nc="nc-settings-subtab-appStore"]`); |
||||
} |
||||
|
||||
async install({ name }: { name: string }) { |
||||
const card = await this.settings.get().locator(`.nc-app-store-card-${name}`); |
||||
await card.click(); |
||||
await card.locator('.nc-app-store-card-install').click(); |
||||
} |
||||
|
||||
async configureSlack() {} |
||||
|
||||
async configureSMTP({ email, host, port }: { email: string; host: string; port: string }) { |
||||
const appStoreCard = this.rootPage.locator('.nc-modal-plugin-install'); |
||||
|
||||
await appStoreCard.locator('[id="form_item_from"]').fill(email); |
||||
await appStoreCard.locator('[id="form_item_host"]').fill(host); |
||||
await appStoreCard.locator('[id="form_item_port"]').fill(port); |
||||
|
||||
await appStoreCard.locator('button:has-text("Save")').click(); |
||||
} |
||||
|
||||
async uninstall(param: { name: string }) { |
||||
const card = this.settings.get().locator(`.nc-app-store-card-${param.name}`); |
||||
|
||||
// await card.scrollIntoViewIfNeeded();
|
||||
await card.click(); |
||||
await card.locator('.nc-app-store-card-reset').click(); |
||||
await this.rootPage.locator('button.ant-btn-dangerous').click(); |
||||
} |
||||
} |
@ -0,0 +1,75 @@
|
||||
import { expect } from '@playwright/test'; |
||||
import { SettingsPage } from '.'; |
||||
import BasePage from '../../Base'; |
||||
|
||||
export class AuditSettingsPage extends BasePage { |
||||
private readonly settings: SettingsPage; |
||||
|
||||
constructor(settings: SettingsPage) { |
||||
super(settings.rootPage); |
||||
this.settings = settings; |
||||
} |
||||
|
||||
get() { |
||||
return this.settings.get().locator(`[data-nc="nc-settings-subtab-Audit"]`); |
||||
} |
||||
|
||||
async verifyRow({ |
||||
index, |
||||
opType, |
||||
opSubtype, |
||||
description, |
||||
user, |
||||
created, |
||||
}: { |
||||
index: number; |
||||
opType?: string; |
||||
opSubtype?: string; |
||||
description?: string; |
||||
user?: string; |
||||
created?: string; |
||||
}) { |
||||
const table = await this.get(); |
||||
const row = table.locator(`tr.ant-table-row`).nth(index); |
||||
|
||||
if (opType) { |
||||
await row |
||||
.locator(`td.ant-table-cell`) |
||||
.nth(0) |
||||
.textContent() |
||||
.then(async text => await expect(text).toContain(opType)); |
||||
} |
||||
|
||||
if (opSubtype) { |
||||
await row |
||||
.locator(`td.ant-table-cell`) |
||||
.nth(1) |
||||
.textContent() |
||||
.then(async text => await expect(text).toContain(opSubtype)); |
||||
} |
||||
|
||||
if (description) { |
||||
await row |
||||
.locator(`td.ant-table-cell`) |
||||
.nth(2) |
||||
.textContent() |
||||
.then(async text => await expect(text).toContain(description)); |
||||
} |
||||
|
||||
if (user) { |
||||
await row |
||||
.locator(`td.ant-table-cell`) |
||||
.nth(3) |
||||
.textContent() |
||||
.then(async text => await expect(text).toContain(user)); |
||||
} |
||||
|
||||
if (created) { |
||||
await row |
||||
.locator(`td.ant-table-cell`) |
||||
.nth(4) |
||||
.textContent() |
||||
.then(async text => await expect(text).toContain(created)); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@
|
||||
import { SettingsPage } from '.'; |
||||
import { ErdBasePage } from '../commonBase/Erd'; |
||||
|
||||
export class SettingsErdPage extends ErdBasePage { |
||||
readonly settings: SettingsPage; |
||||
|
||||
constructor(settings: SettingsPage) { |
||||
super(settings.rootPage); |
||||
this.settings = settings; |
||||
} |
||||
|
||||
get() { |
||||
return this.rootPage.locator(`[data-nc="nc-settings-subtab-ERD View"]`); |
||||
} |
||||
} |
@ -0,0 +1,48 @@
|
||||
import { expect } from '@playwright/test'; |
||||
import { SettingsPage } from '.'; |
||||
import BasePage from '../../Base'; |
||||
|
||||
export class MetaDataPage extends BasePage { |
||||
private readonly settings: SettingsPage; |
||||
|
||||
constructor(settings: SettingsPage) { |
||||
super(settings.rootPage); |
||||
this.settings = settings; |
||||
} |
||||
|
||||
get() { |
||||
return this.settings.get().locator(`[data-nc="nc-settings-subtab-Metadata"]`); |
||||
} |
||||
|
||||
async clickReload() { |
||||
await this.get().locator(`button:has-text("Reload")`).click(); |
||||
|
||||
// todo: Remove this wait
|
||||
await this.rootPage.waitForTimeout(100); |
||||
// await this.get().locator(`.animate-spin`).waitFor({state: 'visible'});
|
||||
await this.get().locator(`.animate-spin`).waitFor({ state: 'detached' }); |
||||
} |
||||
|
||||
async sync() { |
||||
await this.get().locator(`button:has-text("Sync Now")`).click(); |
||||
await this.verifyToast({ message: 'Table metadata recreated successfully' }); |
||||
await this.get().locator(`.animate-spin`).waitFor({ state: 'visible' }); |
||||
await this.get().locator(`.animate-spin`).waitFor({ state: 'detached' }); |
||||
} |
||||
|
||||
async verifyRow({ index, model, state }: { index: number; model: string; state: string }) { |
||||
await expect |
||||
.poll(async () => { |
||||
return await this.get() |
||||
.locator(`tr.ant-table-row`) |
||||
.nth(index) |
||||
.locator(`td.ant-table-cell`) |
||||
.nth(0) |
||||
.textContent(); |
||||
}) |
||||
.toContain(model); |
||||
await expect( |
||||
await this.get().locator(`tr.ant-table-row`).nth(index).locator(`td.ant-table-cell`).nth(1).textContent() |
||||
).toContain(state); |
||||
} |
||||
} |
@ -0,0 +1,20 @@
|
||||
import { expect } from '@playwright/test'; |
||||
import { SettingsPage } from '.'; |
||||
import BasePage from '../../Base'; |
||||
|
||||
export class MiscSettingsPage extends BasePage { |
||||
private readonly settings: SettingsPage; |
||||
|
||||
constructor(settings: SettingsPage) { |
||||
super(settings.rootPage); |
||||
this.settings = settings; |
||||
} |
||||
|
||||
get() { |
||||
return this.settings.get().locator(`[data-nc="nc-settings-subtab-Miscellaneous"]`); |
||||
} |
||||
|
||||
async clickShowM2MTables() { |
||||
await this.get().locator('input[type="checkbox"]').click(); |
||||
} |
||||
} |
@ -0,0 +1,99 @@
|
||||
import { expect, Locator } from '@playwright/test'; |
||||
import { SettingsPage } from '.'; |
||||
import BasePage from '../../Base'; |
||||
import { writeFileAsync } from 'xlsx'; |
||||
import { ToolbarPage } from '../common/Toolbar'; |
||||
|
||||
export class TeamsPage extends BasePage { |
||||
private readonly settings: SettingsPage; |
||||
readonly inviteTeamBtn: Locator; |
||||
readonly inviteTeamModal: Locator; |
||||
|
||||
constructor(settings: SettingsPage) { |
||||
super(settings.rootPage); |
||||
this.settings = settings; |
||||
this.inviteTeamBtn = this.get().locator(`button:has-text("Invite Team")`); |
||||
this.inviteTeamModal = this.rootPage.locator(`.nc-modal-invite-user-and-share-base`); |
||||
} |
||||
|
||||
get() { |
||||
return this.settings.get().locator(`[data-nc="nc-settings-subtab-Users Management"]`); |
||||
} |
||||
|
||||
prefixEmail(email: string) { |
||||
const parallelId = process.env.TEST_PARALLEL_INDEX ?? '0'; |
||||
return `nc_test_${parallelId}_${email}`; |
||||
} |
||||
|
||||
getSharedBaseSubModal() { |
||||
return this.rootPage.locator(`[data-nc="nc-share-base-sub-modal"]`); |
||||
} |
||||
|
||||
async invite({ email, role }: { email: string; role: string }) { |
||||
email = this.prefixEmail(email); |
||||
|
||||
await this.inviteTeamBtn.click(); |
||||
await this.inviteTeamModal.locator(`input[placeholder="E-mail"]`).fill(email); |
||||
await this.inviteTeamModal.locator(`.nc-user-roles`).click(); |
||||
const userRoleModal = this.rootPage.locator(`.nc-dropdown-user-role`); |
||||
await userRoleModal.locator(`.nc-role-option:has-text("${role}")`).click(); |
||||
await this.inviteTeamModal.locator(`button:has-text("Invite")`).click(); |
||||
await this.verifyToast({ message: 'Successfully updated the user details' }); |
||||
|
||||
return await this.inviteTeamModal.locator(`.ant-alert-message`).innerText(); |
||||
} |
||||
|
||||
async closeInvite() { |
||||
// two btn-icon-only in invite modal: close & copy url
|
||||
await this.inviteTeamModal.locator(`button.ant-btn-icon-only:visible`).first().click(); |
||||
} |
||||
|
||||
async inviteMore() { |
||||
await this.inviteTeamModal.locator(`button:has-text("Invite More")`).click(); |
||||
} |
||||
|
||||
async toggleSharedBase({ toggle }: { toggle: boolean }) { |
||||
const toggleBtn = await this.getSharedBaseSubModal().locator(`.nc-disable-shared-base`); |
||||
const toggleBtnText = await toggleBtn.first().innerText(); |
||||
|
||||
const disabledBase = toggleBtnText.includes('Disable'); |
||||
|
||||
if (disabledBase) { |
||||
if (toggle) { |
||||
// if share base was disabled && request was to enable
|
||||
await toggleBtn.click(); |
||||
const modal = await this.rootPage.locator(`.nc-dropdown-shared-base-toggle`); |
||||
await modal.locator(`.ant-dropdown-menu-title-content`).click(); |
||||
} |
||||
} else { |
||||
if (!toggle) { |
||||
// if share base was enabled && request was to disable
|
||||
await toggleBtn.click(); |
||||
const modal = await this.rootPage.locator(`.nc-dropdown-shared-base-toggle`); |
||||
await modal.locator(`.ant-dropdown-menu-title-content`).click(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async getSharedBaseUrl() { |
||||
const url = await this.getSharedBaseSubModal().locator(`.nc-url:visible`).innerText(); |
||||
return url; |
||||
} |
||||
|
||||
async sharedBaseActions({ action }: { action: string }) { |
||||
const actionMenu = ['reload', 'copy url', 'open tab', 'copy embed code']; |
||||
const index = actionMenu.indexOf(action); |
||||
|
||||
await this.getSharedBaseSubModal().locator(`button.ant-btn-icon-only`).nth(index).click(); |
||||
} |
||||
|
||||
async sharedBaseRole({ role }: { role: string }) { |
||||
// editor | viewer
|
||||
// await this.getSharedBaseSubModal()
|
||||
// .locator(`.nc-shared-base-role`)
|
||||
// .waitFor();
|
||||
await this.getSharedBaseSubModal().locator(`.nc-shared-base-role:visible`).click(); |
||||
const userRoleModal = await this.rootPage.locator(`.nc-dropdown-share-base-role:visible`); |
||||
await userRoleModal.locator(`.ant-select-item-option-content:has-text("${role}"):visible`).click(); |
||||
} |
||||
} |
@ -0,0 +1,63 @@
|
||||
import { DashboardPage } from '..'; |
||||
import BasePage from '../../Base'; |
||||
import { AuditSettingsPage } from './Audit'; |
||||
import { SettingsErdPage } from './Erd'; |
||||
import { MetaDataPage } from './Metadata'; |
||||
import { AppStoreSettingsPage } from './AppStore'; |
||||
import { MiscSettingsPage } from './Miscellaneous'; |
||||
import { TeamsPage } from './Teams'; |
||||
import { AclPage } from './Acl'; |
||||
|
||||
export enum SettingTab { |
||||
TeamAuth = 'teamAndAuth', |
||||
AppStore = 'appStore', |
||||
ProjectMetadata = 'projMetaData', |
||||
Audit = 'audit', |
||||
} |
||||
|
||||
export enum SettingsSubTab { |
||||
ERD = 'erd', |
||||
Miscellaneous = 'misc', |
||||
ACL = 'acl', |
||||
} |
||||
|
||||
export class SettingsPage extends BasePage { |
||||
private readonly dashboard: DashboardPage; |
||||
readonly audit: AuditSettingsPage; |
||||
readonly appStore: AppStoreSettingsPage; |
||||
readonly metaData: MetaDataPage; |
||||
readonly miscellaneous: MiscSettingsPage; |
||||
readonly erd: SettingsErdPage; |
||||
readonly teams: TeamsPage; |
||||
readonly acl: AclPage; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
this.audit = new AuditSettingsPage(this); |
||||
this.appStore = new AppStoreSettingsPage(this); |
||||
this.metaData = new MetaDataPage(this); |
||||
this.miscellaneous = new MiscSettingsPage(this); |
||||
this.erd = new SettingsErdPage(this); |
||||
this.teams = new TeamsPage(this); |
||||
this.acl = new AclPage(this); |
||||
} |
||||
|
||||
get() { |
||||
return this.rootPage.locator('.nc-modal-settings'); |
||||
} |
||||
|
||||
async selectTab({ tab, subTab }: { tab: SettingTab; subTab?: SettingsSubTab }) { |
||||
await this.get().locator(`li[data-menu-id="${tab}"]`).click(); |
||||
if (subTab) await this.get().locator(`li[data-menu-id="${subTab}"]`).click(); |
||||
} |
||||
|
||||
async selectSubTab({ subTab }: { subTab: SettingsSubTab }) { |
||||
await this.get().locator(`li[data-menu-id="${subTab}"]`).click(); |
||||
} |
||||
|
||||
async close() { |
||||
await this.get().locator('[data-nc="settings-modal-close-button"]').click(); |
||||
await this.get().waitFor({ state: 'hidden' }); |
||||
} |
||||
} |
@ -0,0 +1,86 @@
|
||||
import { expect, Locator, Page } from '@playwright/test'; |
||||
import BasePage from '../../Base'; |
||||
|
||||
export class SurveyFormPage extends BasePage { |
||||
readonly formHeading: Locator; |
||||
readonly formSubHeading: Locator; |
||||
readonly submitButton: Locator; |
||||
readonly nextButton: Locator; |
||||
readonly nextSlideButton: Locator; |
||||
readonly prevSlideButton: Locator; |
||||
readonly darkModeButton: Locator; |
||||
readonly formFooter: Locator; |
||||
|
||||
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"]'); |
||||
} |
||||
|
||||
get() { |
||||
return this.rootPage.locator('html >> .nc-form-view'); |
||||
} |
||||
|
||||
async validate({ |
||||
heading, |
||||
subHeading, |
||||
fieldLabel, |
||||
footer, |
||||
}: { |
||||
heading: string; |
||||
subHeading: string; |
||||
fieldLabel: string; |
||||
footer: string; |
||||
}) { |
||||
await expect(this.get()).toBeVisible(); |
||||
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); |
||||
|
||||
// parse footer text ("1 / 3") to identify if last slide
|
||||
let isLastSlide = false; |
||||
const footerText = await this.formFooter.innerText(); |
||||
const slideNumber = footerText.split(' / ')[0]; |
||||
const totalSlides = footerText.split(' / ')[1]; |
||||
if (slideNumber === totalSlides) { |
||||
isLastSlide = true; |
||||
} |
||||
if (isLastSlide) { |
||||
await expect(this.submitButton).toBeVisible(); |
||||
} else { |
||||
await expect(this.nextButton).toBeVisible(); |
||||
} |
||||
} |
||||
|
||||
async fill(param: { fieldLabel: string; type?: string; value?: string }) { |
||||
await this.get().locator(`[data-nc="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); |
||||
// press enter key
|
||||
await this.get().locator(`[data-nc="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(); |
||||
await modal.locator('.ant-picker-now-btn').click(); |
||||
await modal.locator('.ant-picker-ok').click(); |
||||
await this.nextButton.click(); |
||||
} |
||||
} |
||||
|
||||
async validateSuccessMessage(param: { message: string; showAnotherForm?: boolean }) { |
||||
await expect( |
||||
this.get().locator(`[data-nc="nc-survey-form__success-msg"]:has-text("${param.message}")`) |
||||
).toBeVisible(); |
||||
|
||||
if (param.showAnotherForm) { |
||||
await expect(this.get().locator(`button:has-text("Submit Another Form")`)).toBeVisible(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,136 @@
|
||||
import { expect, Locator } from '@playwright/test'; |
||||
import { DashboardPage } from '.'; |
||||
import BasePage from '../Base'; |
||||
|
||||
export class TreeViewPage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly project: any; |
||||
readonly quickImportButton: Locator; |
||||
readonly inviteTeamButton: Locator; |
||||
|
||||
constructor(dashboard: DashboardPage, project: any) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
this.project = project; |
||||
this.quickImportButton = dashboard.get().locator('.nc-import-menu'); |
||||
this.inviteTeamButton = dashboard.get().locator('.nc-share-base'); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator('.nc-treeview-container'); |
||||
} |
||||
|
||||
async focusTable({ title }: { title: string }) { |
||||
await this.get().locator(`.nc-project-tree-tbl-${title}`).focus(); |
||||
} |
||||
|
||||
// assumption: first view rendered is always GRID
|
||||
//
|
||||
async openTable({ title, mode = 'standard' }: { title: string; mode?: string }) { |
||||
if ((await this.get().locator('.active.nc-project-tree-tbl').count()) > 0) { |
||||
if ((await this.get().locator('.active.nc-project-tree-tbl').innerText()) === title) { |
||||
// table already open
|
||||
return; |
||||
} |
||||
} |
||||
|
||||
await this.waitForResponse({ |
||||
uiAction: this.get().locator(`.nc-project-tree-tbl-${title}`).click(), |
||||
httpMethodsToMatch: ['GET'], |
||||
requestUrlPathToMatch: `/api/v1/db/data/noco/`, |
||||
responseJsonMatcher: json => json.pageInfo, |
||||
}); |
||||
await this.dashboard.waitForTabRender({ title, mode }); |
||||
} |
||||
|
||||
async createTable({ title }: { title: string }) { |
||||
await this.get().locator('.nc-add-new-table').click(); |
||||
|
||||
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.waitForResponse({ |
||||
uiAction: this.dashboard.get().locator('button:has-text("Submit")').click(), |
||||
httpMethodsToMatch: ['POST'], |
||||
requestUrlPathToMatch: `/api/v1/db/meta/projects/`, |
||||
responseJsonMatcher: json => json.title === title && json.type === 'table', |
||||
}); |
||||
|
||||
await this.dashboard.waitForTabRender({ title }); |
||||
} |
||||
|
||||
async verifyTable({ title, index, exists = true }: { title: string; index?: number; exists?: boolean }) { |
||||
if (exists) { |
||||
await expect(this.get().locator(`.nc-project-tree-tbl-${title}`)).toBeVisible(); |
||||
|
||||
if (index) { |
||||
await expect(await this.get().locator('.nc-tbl-title').nth(index)).toHaveText(title); |
||||
} |
||||
} else { |
||||
await expect(this.get().locator(`.nc-project-tree-tbl-${title}`)).toHaveCount(0); |
||||
} |
||||
} |
||||
|
||||
async deleteTable({ title }: { title: string }) { |
||||
await this.get().locator(`.nc-project-tree-tbl-${title}`).click({ button: 'right' }); |
||||
await this.dashboard.get().locator('div.nc-project-menu-item:has-text("Delete")').click(); |
||||
|
||||
await this.waitForResponse({ |
||||
uiAction: this.dashboard.get().locator('button:has-text("Yes")').click(), |
||||
httpMethodsToMatch: ['DELETE'], |
||||
requestUrlPathToMatch: `/api/v1/db/meta/tables/`, |
||||
}); |
||||
|
||||
await expect |
||||
.poll( |
||||
async () => |
||||
await this.dashboard.tabBar |
||||
.locator('.ant-tabs-tab', { |
||||
hasText: title, |
||||
}) |
||||
.isVisible() |
||||
) |
||||
.toBe(false); |
||||
|
||||
(await this.rootPage.locator('.nc-container').last().elementHandle())?.waitForElementState('stable'); |
||||
} |
||||
|
||||
async renameTable({ title, newTitle }: { title: string; newTitle: string }) { |
||||
await this.get().locator(`.nc-project-tree-tbl-${title}`).click({ button: 'right' }); |
||||
await this.dashboard.get().locator('div.nc-project-menu-item:has-text("Rename")').click(); |
||||
await this.dashboard.get().locator('[placeholder="Enter table name"]').fill(newTitle); |
||||
await this.dashboard.get().locator('button:has-text("Submit")').click(); |
||||
await this.verifyToast({ message: 'Table renamed successfully' }); |
||||
} |
||||
|
||||
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}"]`)); |
||||
} |
||||
|
||||
async quickImport({ title }: { title: string }) { |
||||
await this.get().locator('.nc-add-new-table').hover(); |
||||
await this.quickImportButton.click(); |
||||
const importMenu = this.dashboard.get().locator('.nc-dropdown-import-menu'); |
||||
await importMenu.locator(`.ant-dropdown-menu-title-content:has-text("${title}")`).click(); |
||||
} |
||||
|
||||
async validateRoleAccess(param: { role: string }) { |
||||
// Add new table button
|
||||
await expect(this.get().locator(`.nc-add-new-table`)).toHaveCount(param.role === 'creator' ? 1 : 0); |
||||
// Import menu
|
||||
await expect(this.get().locator(`.nc-import-menu`)).toHaveCount(param.role === 'creator' ? 1 : 0); |
||||
// Invite Team button
|
||||
await expect(this.get().locator(`.nc-share-base`)).toHaveCount(param.role === 'creator' ? 1 : 0); |
||||
// Right click context menu
|
||||
await this.get().locator(`.nc-project-tree-tbl-Country`).click({ |
||||
button: 'right', |
||||
}); |
||||
await expect(this.rootPage.locator(`.nc-dropdown-tree-view-context-menu:visible`)).toHaveCount( |
||||
param.role === 'creator' ? 1 : 0 |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,132 @@
|
||||
import { expect, Locator } from '@playwright/test'; |
||||
import { DashboardPage } from '../'; |
||||
import BasePage from '../../Base'; |
||||
|
||||
export class ViewSidebarPage extends BasePage { |
||||
readonly project: any; |
||||
readonly dashboard: DashboardPage; |
||||
readonly createGalleryButton: Locator; |
||||
readonly createGridButton: Locator; |
||||
readonly createFormButton: Locator; |
||||
readonly createKanbanButton: Locator; |
||||
|
||||
constructor(dashboard: DashboardPage) { |
||||
super(dashboard.rootPage); |
||||
this.dashboard = dashboard; |
||||
this.createGalleryButton = this.get().locator('.nc-create-gallery-view:visible'); |
||||
this.createGridButton = this.get().locator('.nc-create-grid-view:visible'); |
||||
this.createFormButton = this.get().locator('.nc-create-form-view:visible'); |
||||
this.createKanbanButton = this.get().locator('.nc-create-kanban-view:visible'); |
||||
} |
||||
|
||||
get() { |
||||
return this.dashboard.get().locator('.nc-view-sidebar'); |
||||
} |
||||
|
||||
private async createView({ title, locator }: { title: string; locator: Locator }) { |
||||
await locator.click(); |
||||
await this.rootPage.locator('input[id="form_item_title"]:visible').fill(title); |
||||
const submitAction = this.rootPage |
||||
.locator('.ant-modal-content') |
||||
.locator('button:has-text("Submit"):visible') |
||||
.click(); |
||||
await this.waitForResponse({ |
||||
httpMethodsToMatch: ['POST'], |
||||
requestUrlPathToMatch: '/api/v1/db/meta/tables/', |
||||
uiAction: submitAction, |
||||
responseJsonMatcher: json => json.title === title, |
||||
}); |
||||
await this.verifyToast({ message: 'View created successfully' }); |
||||
// Todo: Wait for view to be rendered
|
||||
await this.rootPage.waitForTimeout(1000); |
||||
} |
||||
|
||||
async createGalleryView({ title }: { title: string }) { |
||||
await this.createView({ title, locator: this.createGalleryButton }); |
||||
} |
||||
|
||||
async createGridView({ title }: { title: string }) { |
||||
await this.createView({ title, locator: this.createGridButton }); |
||||
} |
||||
|
||||
async createFormView({ title }: { title: string }) { |
||||
await this.createView({ title, locator: this.createFormButton }); |
||||
} |
||||
|
||||
async openView({ title }: { title: string }) { |
||||
await this.get().locator(`[data-nc="view-sidebar-view-${title}"]`).click(); |
||||
} |
||||
|
||||
async createKanbanView({ title }: { title: string }) { |
||||
await this.createView({ title, locator: this.createKanbanButton }); |
||||
} |
||||
|
||||
// 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"]') |
||||
).toHaveText(title, { ignoreCase: true }); |
||||
} |
||||
|
||||
async verifyViewNotPresent({ title, index }: { title: string; index: number }) { |
||||
const viewList = this.get().locator(`.nc-views-menu`).locator('.ant-menu-title-content'); |
||||
if ((await viewList.count()) <= index) { |
||||
return true; |
||||
} |
||||
|
||||
return await expect( |
||||
this.get().locator(`.nc-views-menu`).locator('.ant-menu-title-content').nth(index) |
||||
).not.toHaveText(title); |
||||
} |
||||
|
||||
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}"]`)); |
||||
} |
||||
|
||||
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.rootPage.locator('.nc-modal-view-delete').locator('button:has-text("Submit"):visible').click(); |
||||
|
||||
// waiting for button to get detached, we will miss toast
|
||||
// await this.rootPage
|
||||
// .locator(".nc-modal-view-delete")
|
||||
// .locator('button:has-text("Submit")')
|
||||
// .waitFor({ state: "detached" });
|
||||
await this.verifyToast({ message: 'View deleted successfully' }); |
||||
} |
||||
|
||||
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().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(); |
||||
const submitAction = this.rootPage |
||||
.locator('.ant-modal-content') |
||||
.locator('button:has-text("Submit"):visible') |
||||
.click(); |
||||
await this.waitForResponse({ |
||||
httpMethodsToMatch: ['POST'], |
||||
requestUrlPathToMatch: '/api/v1/db/meta/tables/', |
||||
uiAction: submitAction, |
||||
}); |
||||
await this.verifyToast({ message: 'View created successfully' }); |
||||
} |
||||
|
||||
async validateRoleAccess(param: { role: string }) { |
||||
const count = param.role === 'creator' ? 1 : 0; |
||||
await expect(this.createGridButton).toHaveCount(count); |
||||
await expect(this.createGalleryButton).toHaveCount(count); |
||||
await expect(this.createFormButton).toHaveCount(count); |
||||
await expect(this.createKanbanButton).toHaveCount(count); |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue