- add extra users to gen_seed

- add some test-ids
- add new log in provider in dev only for testing
- dont rate limit in dev
- remove testids from production builds
- add test npm scripts
- user auth utils
- setup script
- example tests
- playwright config
This commit is contained in:
Brett Woodward
2025-02-07 11:06:01 -05:00
parent 9384d2a8d2
commit 69ecd2c555
18 changed files with 2155 additions and 265 deletions
+1 -1
View File
@@ -73,8 +73,8 @@ trace/types.json
/src/local
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/tests/auth/*.json
+4
View File
@@ -60,6 +60,10 @@ run:
.PHONY: init
init: copy-env npm-install start run-migrations bootstrap-db bootstrap-metrics run
.PHONY: rerun
rerun: start bootstrap-db
npm run dev
.PHONY: init-devcontainer
init-devcontainer: copy-env npm-install run-migrations bootstrap-db bootstrap-metrics
+251 -240
View File
@@ -1,11 +1,11 @@
// @ts-check
import { withAxiom } from "@civitai/next-axiom";
import { withAxiom } from '@civitai/next-axiom';
import bundlAnalyzer from '@next/bundle-analyzer';
import packageJson from './package.json' assert { type: 'json' };
import bundlAnalyzer from '@next/bundle-analyzer'
const withBundleAnalyzer = bundlAnalyzer({
enabled: process.env.ANALYZE === 'true',
})
});
/**
* Don't be scared of the generics here.
@@ -19,246 +19,257 @@ function defineNextConfig(config) {
return withBundleAnalyzer(config);
}
export default defineNextConfig(withAxiom({
env: {
version: packageJson.version,
},
reactStrictMode: true,
productionBrowserSourceMaps: true,
// Next.js i18n docs: https://nextjs.org/docs/advanced-features/i18n-routing
i18n: {
locales: ['en'],
defaultLocale: 'en',
},
eslint: {
ignoreDuringBuilds: true,
},
generateEtags: false,
compress: false,
images: {
remotePatterns: [
{ hostname: 's3.us-west-1.wasabisys.com', },
{ hostname: 'model-share.s3.us-west-1.wasabisys.com', },
{ hostname: 'civitai-prod.s3.us-west-1.wasabisys.com', },
{ hostname: 'civitai-dev.s3.us-west-1.wasabisys.com', },
{ hostname: 'image.civitai.com', },
]
// domains: [
// 's3.us-west-1.wasabisys.com',
// 'model-share.s3.us-west-1.wasabisys.com',
// 'civitai-prod.s3.us-west-1.wasabisys.com',
// 'civitai-dev.s3.us-west-1.wasabisys.com',
// 'image.civitai.com',
// ],
},
transpilePackages: ['lodash', 'lodash-es', 'prisma'],
experimental: {
// scrollRestoration: true,
largePageDataBytes: 512 * 100000,
optimizePackageImports: [
'@civitai/client',
'./srs/libs/form'
]
},
headers: async () => {
// Add X-Robots-Tag header to all pages matching /sitemap.xml and /sitemap-models.xml /sitemap-articles.xml, etc
const headers = [{
source: '/sitemap(-\\w+)?.xml',
headers: [
{ key: 'X-Robots-Tag', value: 'noindex' },
{ key: 'Content-Type', value: 'application/xml' },
{ key: 'Cache-Control', value: 'public, max-age=86400, must-revalidate' }
export default defineNextConfig(
withAxiom({
env: {
version: packageJson.version,
},
reactStrictMode: true,
productionBrowserSourceMaps: true,
// Next.js i18n docs: https://nextjs.org/docs/advanced-features/i18n-routing
i18n: {
locales: ['en'],
defaultLocale: 'en',
},
eslint: {
ignoreDuringBuilds: true,
},
generateEtags: false,
compress: false,
images: {
remotePatterns: [
{ hostname: 's3.us-west-1.wasabisys.com' },
{ hostname: 'model-share.s3.us-west-1.wasabisys.com' },
{ hostname: 'civitai-prod.s3.us-west-1.wasabisys.com' },
{ hostname: 'civitai-dev.s3.us-west-1.wasabisys.com' },
{ hostname: 'image.civitai.com' },
],
}];
// domains: [
// 's3.us-west-1.wasabisys.com',
// 'model-share.s3.us-west-1.wasabisys.com',
// 'civitai-prod.s3.us-west-1.wasabisys.com',
// 'civitai-dev.s3.us-west-1.wasabisys.com',
// 'image.civitai.com',
// ],
},
compiler:
process.env.NODE_ENV === 'production'
? {
reactRemoveProperties: { properties: ['^data-testid$'] },
// removeConsole: true,
}
: {},
transpilePackages: ['lodash', 'lodash-es', 'prisma'],
experimental: {
// scrollRestoration: true,
largePageDataBytes: 512 * 100000,
optimizePackageImports: ['@civitai/client', './srs/libs/form'],
},
headers: async () => {
// Add X-Robots-Tag header to all pages matching /sitemap.xml and /sitemap-models.xml /sitemap-articles.xml, etc
const headers = [
{
source: '/sitemap(-\\w+)?.xml',
headers: [
{ key: 'X-Robots-Tag', value: 'noindex' },
{ key: 'Content-Type', value: 'application/xml' },
{ key: 'Cache-Control', value: 'public, max-age=86400, must-revalidate' },
],
},
];
if (process.env.NODE_ENV !== 'production') {
headers.push({
source: '/:path*',
headers: [
{
key: 'X-Robots-Tag',
value: 'noindex',
},
],
});
}
if (process.env.NODE_ENV !== 'production') {
headers.push({
source: '/:path*',
headers: [{
key: 'X-Robots-Tag',
value: 'noindex',
}],
headers: [{ key: 'X-Frame-Options', value: 'DENY' }],
});
}
headers.push({
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
],
})
return headers;
},
poweredByHeader: false,
redirects: async () => {
return [
{
source: '/api/download/training-data/:modelVersionId',
destination: '/api/download/models/:modelVersionId?type=Training%20Data',
permanent: true,
},
{
source: '/github/:path*',
destination: 'https://github.com/civitai/civitai/:path*',
permanent: true,
},
{
source: '/discord',
destination: 'https://discord.gg/civitai',
permanent: true,
},
{
source: '/twitter',
destination: 'https://twitter.com/HelloCivitai',
permanent: true,
},
{
source: '/reddit',
destination: 'https://reddit.com/r/civitai',
permanent: true,
},
{
source: '/instagram',
destination: 'https://www.instagram.com/hellocivitai/',
permanent: true,
},
{
source: '/tiktok',
destination: 'https://www.tiktok.com/@hellocivitai',
permanent: true,
},
{
source: '/youtube',
destination: 'https://www.youtube.com/@civitai',
permanent: true,
},
{
source: '/twitch',
destination: 'https://www.twitch.tv/civitai',
permanent: true,
},
{
source: '/ideas',
destination: 'https://github.com/civitai/civitai/discussions/categories/ideas',
permanent: true,
},
{
source: '/v/civitai-link-intro',
destination: 'https://youtu.be/EHUjiDgh-MI',
permanent: false,
},
{
source: '/v/civitai-link-installation',
destination: 'https://youtu.be/fs-Zs-fvxb0',
permanent: false,
},
{
source: '/gallery/:path*',
destination: '/images/:path*',
permanent: true,
},
{
source: '/appeal',
destination: 'https://forms.clickup.com/8459928/f/825mr-5844/5NXSA2EIT3YOS2JSF7',
permanent: true,
},
{
source: '/canny/feedback',
destination: 'https://feedback.civitai.com/?b=feature-request',
permanent: true,
},
{
source: '/feedback',
destination: 'https://feedback.civitai.com/?b=feature-request',
permanent: true,
},
{
source: '/canny/bugs',
destination: 'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
permanent: true,
},
{
source: '/bugs',
destination: 'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
permanent: true,
},
{
source: '/support-portal',
destination: 'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
permanent: true,
},
{
source: '/leaderboard',
destination: '/leaderboard/overall',
permanent: true,
},
{
source: '/forms/bounty-refund',
destination: 'https://forms.clickup.com/8459928/f/825mr-8331/R30FGV9JFHLF527GGN',
permanent: true,
},
{
source: '/forms/press-inquiry',
destination: 'https://forms.clickup.com/8459928/f/825mr-9351/RZXWRNLV9Q1D32ACNP',
permanent: true,
},
{
source: '/forms/matching-partner',
destination: 'https://forms.clickup.com/8459928/f/825mr-9431/IQOAS1RXWHI1E2I1S3',
permanent: true,
},
{
source: '/holiday2023',
destination: '/events/holiday2023',
permanent: true,
},
{
source: '/blocked-by-octoml',
destination: '/articles/3307',
permanent: true,
},
{
source: '/blocked-by-provider',
destination: '/articles/3307',
permanent: true,
},
{
source: '/wiki',
destination: 'https://wiki.civitai.com',
permanent: true,
},
{
source: '/education',
destination: 'https://education.civitai.com',
permanent: true,
},
{
source: '/advertise-with-us',
destination: 'https://forms.clickup.com/8459928/f/825mr-10211/MIN35AIDXBZ7BTD5MG',
permanent: true,
},
{
source: '/ad-feedback',
destination: 'https://forms.clickup.com/8459928/f/825mr-9711/WIMNO6V738T4ZBTPXP',
permanent: true,
},
{
source: '/cosmetic-shop',
destination: '/shop',
permanent: true,
},
{
source: '/shop/cosmetic-shop',
destination: '/shop',
permanent: true,
},
{
source: '/projectodyssey_season2',
destination: '/collections/6503138',
permanent: true,
}
];
},
output: 'standalone',
}));
return headers;
},
poweredByHeader: false,
redirects: async () => {
return [
{
source: '/api/download/training-data/:modelVersionId',
destination: '/api/download/models/:modelVersionId?type=Training%20Data',
permanent: true,
},
{
source: '/github/:path*',
destination: 'https://github.com/civitai/civitai/:path*',
permanent: true,
},
{
source: '/discord',
destination: 'https://discord.gg/civitai',
permanent: true,
},
{
source: '/twitter',
destination: 'https://twitter.com/HelloCivitai',
permanent: true,
},
{
source: '/reddit',
destination: 'https://reddit.com/r/civitai',
permanent: true,
},
{
source: '/instagram',
destination: 'https://www.instagram.com/hellocivitai/',
permanent: true,
},
{
source: '/tiktok',
destination: 'https://www.tiktok.com/@hellocivitai',
permanent: true,
},
{
source: '/youtube',
destination: 'https://www.youtube.com/@civitai',
permanent: true,
},
{
source: '/twitch',
destination: 'https://www.twitch.tv/civitai',
permanent: true,
},
{
source: '/ideas',
destination: 'https://github.com/civitai/civitai/discussions/categories/ideas',
permanent: true,
},
{
source: '/v/civitai-link-intro',
destination: 'https://youtu.be/EHUjiDgh-MI',
permanent: false,
},
{
source: '/v/civitai-link-installation',
destination: 'https://youtu.be/fs-Zs-fvxb0',
permanent: false,
},
{
source: '/gallery/:path*',
destination: '/images/:path*',
permanent: true,
},
{
source: '/appeal',
destination: 'https://forms.clickup.com/8459928/f/825mr-5844/5NXSA2EIT3YOS2JSF7',
permanent: true,
},
{
source: '/canny/feedback',
destination: 'https://feedback.civitai.com/?b=feature-request',
permanent: true,
},
{
source: '/feedback',
destination: 'https://feedback.civitai.com/?b=feature-request',
permanent: true,
},
{
source: '/canny/bugs',
destination:
'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
permanent: true,
},
{
source: '/bugs',
destination:
'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
permanent: true,
},
{
source: '/support-portal',
destination:
'https://civitai-team.myfreshworks.com/login/auth/civitai?client_id=451979510707337272&redirect_uri=https%3A%2F%2Fcivitai.freshdesk.com%2Ffreshid%2Fcustomer_authorize_callback%3Fhd%3Dsupport.civitai.com',
permanent: true,
},
{
source: '/leaderboard',
destination: '/leaderboard/overall',
permanent: true,
},
{
source: '/forms/bounty-refund',
destination: 'https://forms.clickup.com/8459928/f/825mr-8331/R30FGV9JFHLF527GGN',
permanent: true,
},
{
source: '/forms/press-inquiry',
destination: 'https://forms.clickup.com/8459928/f/825mr-9351/RZXWRNLV9Q1D32ACNP',
permanent: true,
},
{
source: '/forms/matching-partner',
destination: 'https://forms.clickup.com/8459928/f/825mr-9431/IQOAS1RXWHI1E2I1S3',
permanent: true,
},
{
source: '/holiday2023',
destination: '/events/holiday2023',
permanent: true,
},
{
source: '/blocked-by-octoml',
destination: '/articles/3307',
permanent: true,
},
{
source: '/blocked-by-provider',
destination: '/articles/3307',
permanent: true,
},
{
source: '/wiki',
destination: 'https://wiki.civitai.com',
permanent: true,
},
{
source: '/education',
destination: 'https://education.civitai.com',
permanent: true,
},
{
source: '/advertise-with-us',
destination: 'https://forms.clickup.com/8459928/f/825mr-10211/MIN35AIDXBZ7BTD5MG',
permanent: true,
},
{
source: '/ad-feedback',
destination: 'https://forms.clickup.com/8459928/f/825mr-9711/WIMNO6V738T4ZBTPXP',
permanent: true,
},
{
source: '/cosmetic-shop',
destination: '/shop',
permanent: true,
},
{
source: '/shop/cosmetic-shop',
destination: '/shop',
permanent: true,
},
{
source: '/projectodyssey_season2',
destination: '/collections/6503138',
permanent: true,
},
];
},
output: 'standalone',
})
);
+3 -3
View File
@@ -40,9 +40,9 @@
"analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build",
"tsc:trace": "tsc --generateTrace trace --incremental false",
"tsc:analyze": "npx analyze-trace trace",
"test": "npx playwright test",
"test:ui": "npx playwright test --ui",
"test:gen": "npx playwright codegen"
"test": "cross-env NODE_ENV=development npx playwright test",
"test:ui": "cross-env NODE_ENV=development npx playwright test --ui",
"test:gen": "cross-env NODE_ENV=development npx playwright codegen"
},
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
+84
View File
@@ -0,0 +1,84 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// if (process.env.NODE_ENV === 'development') {
// dotenv.config({
// path: ['.env.development.local', '.env.local', '.env.development', '.env'],
// override: false,
// });
// }
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
// workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
// reporter: 'html',
reporter: process.env.CI ? 'html' : 'line',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
// Setup project
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
dependencies: ['setup'],
},
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
+229 -7
View File
@@ -37,7 +37,7 @@ import {
import { checkLocalDb, insertRows } from './utils';
// import { fetchBlob } from '~/utils/file-utils';
const numRows = 1000;
const numRows = 200;
faker.seed(1337);
const randw = faker.helpers.weightedArrayElement;
@@ -157,11 +157,11 @@ const truncateNotificationRows = async () => {
const genUsers = (num: number, includeCiv = false) => {
const ret = [];
if (includeCiv) {
num -= 1;
const extraUsers = [];
if (includeCiv) {
// civ user
const civUser = [
extraUsers.push([
'Civitai',
'hello@civitai.com',
null,
@@ -194,15 +194,237 @@ const genUsers = (num: number, includeCiv = false) => {
null,
'Eligible',
null,
];
]);
ret.push(civUser);
// - test users
// mod
extraUsers.push([
'Test - Moderator', // name
'test-mod@civitai.com', // email
null,
null,
1, // id
false, // blurnsfw
true, // shownsfw
'test-mod', // username
true, // isMod
false,
'2022-11-13 00:00:00.000',
null, // deletedAt
null, // bannedAt
null,
null,
true,
'{"fp": "fp16", "size": "pruned", "format": "SafeTensor"}',
null,
'{Moderation,Buzz}', // onboardingSteps
null,
'{}', // meta
'{}', // settings
null, // "mutedAt"
false, // muted
31, // "browsingLevel"
15, // onboarding
'{}', // "publicSettings"
null, // "muteConfirmedAt"
false,
null,
'Eligible',
`ctm_01j6${faker.string.alphanumeric(22)}`,
]);
// newbie
extraUsers.push([
'Test - Newbie', // name
'test-newbie@civitai.com', // email
null,
null,
2, // id
true, // blurnsfw
false, // shownsfw
'test-newbie', // username
false, // isMod
false,
'2024-11-13 00:00:00.000',
null, // deletedAt
null, // bannedAt
null,
null,
false,
'{"fp": "fp16", "size": "pruned", "format": "SafeTensor"}',
null,
'{Moderation,Buzz}',
null,
'{}', // meta
'{}', // settings
null, // "mutedAt"
false, // muted
1, // "browsingLevel"
0, // onboarding
'{}', // "publicSettings"
null, // "muteConfirmedAt"
false,
null,
'Eligible',
null,
]);
// degen
extraUsers.push([
'Test - Degen', // name
'test-degen@civitai.com', // email
'2023-11-14 00:00:00.000',
null,
3, // id
false, // blurnsfw
true, // shownsfw
'test-degen', // username
false, // isMod
false,
'2023-11-13 00:00:00.000',
null, // deletedAt
null, // bannedAt
null,
null,
true,
'{"fp": "fp16", "size": "pruned", "format": "SafeTensor"}',
null,
'{}',
null,
'{"scores": {"total": 374, "users": 300, "images": 70, "models": 4, "reportsActioned": 50}}', // meta
'{}', // settings
null, // "mutedAt"
false, // muted
31, // "browsingLevel"
15, // onboarding
'{}', // "publicSettings"
null, // "muteConfirmedAt"
false,
null,
'Eligible',
`ctm_01j6${faker.string.alphanumeric(22)}`,
]);
// banned
extraUsers.push([
'Test - Banned', // name
'test-banned@civitai.com', // email
'2023-11-14 00:00:00.000',
null,
4, // id
false, // blurnsfw
true, // shownsfw
'test-banned', // username
false, // isMod
false,
'2023-11-13 00:00:00.000',
null, // deletedAt
'2023-11-17 00:00:00.000', // bannedAt
null,
null,
true,
'{"fp": "fp16", "size": "pruned", "format": "SafeTensor"}',
null,
'{}',
null,
'{}', // meta
'{}', // settings
null, // "mutedAt"
false, // muted
31, // "browsingLevel"
15, // onboarding
'{}', // "publicSettings"
null, // "muteConfirmedAt"
false,
null,
'Eligible',
`ctm_01j6${faker.string.alphanumeric(22)}`,
]);
// deleted
extraUsers.push([
'Test - Deleted', // name
'test-deleted@civitai.com', // email
'2023-11-14 00:00:00.000',
null,
5, // id
false, // blurnsfw
true, // shownsfw
'test-deleted', // username
false, // isMod
false,
'2023-11-13 00:00:00.000',
'2023-11-17 00:00:00.000', // deletedAt
null, // bannedAt
null,
null,
true,
'{"fp": "fp16", "size": "pruned", "format": "SafeTensor"}',
null,
'{}',
null,
'{}', // meta
'{}', // settings
null, // "mutedAt"
false, // muted
31, // "browsingLevel"
15, // onboarding
'{}', // "publicSettings"
null, // "muteConfirmedAt"
false,
null,
'Eligible',
`ctm_01j6${faker.string.alphanumeric(22)}`,
]);
// muted
extraUsers.push([
'Test - Muted', // name
'test-muted@civitai.com', // email
'2023-11-14 00:00:00.000',
null,
6, // id
false, // blurnsfw
true, // shownsfw
'test-muted', // username
false, // isMod
false,
'2023-11-13 00:00:00.000',
null, // deletedAt
null, // bannedAt
null,
null,
true,
'{"fp": "fp16", "size": "pruned", "format": "SafeTensor"}',
null,
'{}',
null,
'{}', // meta
'{}', // settings
'2023-11-17 00:00:00.000', // "mutedAt"
true, // muted
31, // "browsingLevel"
15, // onboarding
'{}', // "publicSettings"
'2023-11-17 01:00:00.000', // "muteConfirmedAt"
false,
null,
'Eligible',
`ctm_01j6${faker.string.alphanumeric(22)}`,
]);
// subscriber
// customerSubscription?
ret.push(...extraUsers);
num += extraUsers.length;
}
const seenUserNames: string[] = [];
// random users
for (let step = 1; step <= num; step++) {
for (let step = extraUsers.length + 1; step <= num; step++) {
const created = faker.date.past({ years: 3 }).toISOString();
const isMuted = fbool(0.01);
let username = faker.internet.userName();
+1
View File
@@ -41,6 +41,7 @@ export function ChatButton() {
<ActionIcon
variant={state.open ? 'filled' : undefined}
onClick={() => setState((prev) => ({ ...prev, open: !state.open }))}
data-testid="open-chat"
>
<IconMessage2 />
</ActionIcon>
@@ -1,19 +1,19 @@
import { Stack, Group, Button, Alert, Center, createStyles } from '@mantine/core';
import { Form, InputRTE, useForm } from '~/libs/form';
import { useRef, useState, useMemo } from 'react';
import { UpsertCommentV2Input, upsertCommentv2Schema } from '~/server/schema/commentv2.schema';
import { showErrorNotification } from '~/utils/notifications';
import { trpc } from '~/utils/trpc';
import produce from 'immer';
import type { EditorCommandsRef } from '~/components/RichTextEditor/RichTextEditorComponent';
import { SimpleUser } from '~/server/selectors/user.selector';
import { Alert, Button, Center, createStyles, Group, Stack } from '@mantine/core';
import { IconLock } from '@tabler/icons-react';
import produce from 'immer';
import { useMemo, useRef, useState } from 'react';
import {
useCommentsContext,
useNewCommentStore,
useRootThreadContext,
} from '~/components/CommentsV2/CommentsProvider';
import type { EditorCommandsRef } from '~/components/RichTextEditor/RichTextEditorComponent';
import { Form, InputRTE, useForm } from '~/libs/form';
import { UpsertCommentV2Input, upsertCommentv2Schema } from '~/server/schema/commentv2.schema';
import { SimpleUser } from '~/server/selectors/user.selector';
import { removeDuplicates } from '~/utils/array-helpers';
import { showErrorNotification } from '~/utils/notifications';
import { trpc } from '~/utils/trpc';
/*
Most use cases of this form will require cancel/submit buttons to be displayed
@@ -173,6 +173,7 @@ export const CommentForm = ({
root: borderless ? 'border-none' : undefined,
content: cx(classes.content, 'rounded-3xl'),
}}
data-testid="comment-form"
/>
{focused && (
<Group position="right">
+3 -3
View File
@@ -1,11 +1,11 @@
import { Center, Loader, createStyles, Stack, Alert, Text } from '@mantine/core';
import { Alert, Center, createStyles, Loader, Stack, Text } from '@mantine/core';
import { IconInbox } from '@tabler/icons-react';
import { GeneratedImage } from '~/components/ImageGeneration/GeneratedImage';
import { useGetTextToImageRequestsImages } from '~/components/ImageGeneration/utils/generationRequestHooks';
import { InViewLoader } from '~/components/InView/InViewLoader';
import { useFiltersContext } from '~/providers/FiltersProvider';
import { generationPanel } from '~/store/generation.store';
import { isDefined } from '~/utils/type-guards';
import { useFiltersContext } from '~/providers/FiltersProvider';
export function Feed() {
const { classes } = useStyles();
@@ -68,7 +68,7 @@ export function Feed() {
return (
<div className="flex flex-col gap-2 px-3">
{/* <GeneratedImagesBuzzPrompt /> */}
<div className={classes.grid}>
<div className={classes.grid} data-testid="generation-feed-list">
{steps.map((step) =>
step.images
.filter((x) => x.status === 'succeeded')
+27
View File
@@ -241,6 +241,33 @@ export function createAuthOptions(req?: AuthedRequest): NextAuthOptions {
}
},
}),
...(isDev
? [
CredentialsProvider({
id: 'testing-login',
name: 'Testing Login',
credentials: {
id: { label: 'id', type: 'text' },
},
async authorize(credentials) {
if (!isDev) return null;
const { id } = credentials ?? {};
if (!id) throw new Error('No id provided.');
try {
const userId = Number(id);
const user = await getSessionUser({ userId });
if (!user) throw new Error('No user found.');
return user;
} catch (e: unknown) {
const err = e as Error;
throw new Error(`Failed to authenticate: ${err.message}.`);
}
},
}),
]
: []),
],
cookies: {
sessionToken: {
+43
View File
@@ -0,0 +1,43 @@
import { Container, Loader, Stack } from '@mantine/core';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { NotFound } from '~/components/AppLayout/NotFound';
import { isDev } from '~/env/other';
export default function DevLoginPage() {
const router = useRouter();
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!isDev) return;
const { userId } = router.query;
if (userId) {
console.log('Logging in as user', userId);
setLoading(true);
signIn('testing-login', { id: userId as string, callbackUrl: '/' })
.then(() => setLoading(false))
.catch(console.error);
}
}, [router.query]);
if (!isDev) return <NotFound />;
return (
<Container size="xs">
<Stack>
<h1>Development Login</h1>
<p>
This page is only available in development mode. Pass the user ID in the query string to
log in as that user.
</p>
<p>
Example: <code>?userId=1</code>
</p>
{loading && <Loader />}
</Stack>
</Container>
);
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { TRPCError } from '@trpc/server';
import { isProd } from '~/env/other';
import { isDev, isProd } from '~/env/other';
import { purgeCache } from '~/server/cloudflare/client';
import { CacheTTL } from '~/server/common/constants';
import { logToAxiom } from '~/server/logging/client';
@@ -106,7 +106,7 @@ export function rateLimit(rateLimits: undefined | RateLimit | RateLimit[]) {
return middleware(async ({ ctx, next, path }) => {
// Skip if user is a moderator
if (ctx.user?.isModerator) return await next();
if (ctx.user?.isModerator || isDev) return await next();
// Get valid limits
let validLimits: RateLimit[] = [];
+3
View File
@@ -148,3 +148,6 @@ export type BuzzWithdrawalGetPaginatedItem =
type ToolRouter = RouterOutput['tool'];
export type ToolGetAllModel = ToolRouter['getAll']['items'][number];
type OrchestratorRouter = RouterOutput['orchestrator'];
export type QueryGeneratedImages = OrchestratorRouter['queryGeneratedImages'];
+46
View File
@@ -0,0 +1,46 @@
import { expect, Page, test as setup } from '@playwright/test';
import fs from 'fs';
import { env } from '../src/env/server';
import { testAuthData } from './auth/data';
// make sure we're using local DB for testing
setup.beforeAll('check db', async () => {
expect(
env.DATABASE_URL.includes('localhost:15432') || env.DATABASE_URL.includes('db:5432')
// ).toBeFalsy();
).toBeTruthy();
});
// save various user info for testing
const authSetup = async (page: Page, d: (typeof testAuthData)[keyof typeof testAuthData]) => {
if (fs.existsSync(d.path)) {
// console.log(`Skipping auth setup for ${d.userId}, file exists: ${d.path}`);
return;
}
console.log(`Setting up user ID: ${d.userId}`);
await page.goto(`/testing/testing-login?userId=${d.userId}`);
await page.waitForURL('/');
// await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible();
await page.context().storageState({ path: d.path });
};
setup('auth as mod', async ({ page }) => {
await authSetup(page, testAuthData.mod);
});
setup('auth as newbie', async ({ page }) => {
await authSetup(page, testAuthData.newbie);
});
setup('auth as degen', async ({ page }) => {
await authSetup(page, testAuthData.degen);
});
setup('auth as banned', async ({ page }) => {
await authSetup(page, testAuthData.banned);
});
// setup('auth as deleted', async ({ page }) => {
// await authSetup(page, testAuthData.deleted);
// });
setup('auth as muted', async ({ page }) => {
await authSetup(page, testAuthData.muted);
});
+36
View File
@@ -0,0 +1,36 @@
const basePath = 'tests/auth';
export const testAuthData = {
mod: {
userId: 1,
path: `${basePath}/mod.json`,
},
newbie: {
userId: 2,
path: `${basePath}/newbie.json`,
},
degen: {
userId: 3,
path: `${basePath}/degen.json`,
},
banned: {
userId: 4,
path: `${basePath}/banned.json`,
},
deleted: {
userId: 5,
path: `${basePath}/deleted.json`,
},
muted: {
userId: 6,
path: `${basePath}/muted.json`,
},
};
export const authEmpty = { storageState: { cookies: [], origins: [] } };
export const authMod = { storageState: testAuthData.mod.path };
export const authNewbie = { storageState: testAuthData.newbie.path };
export const authDegen = { storageState: testAuthData.degen.path };
export const authBanned = { storageState: testAuthData.banned.path };
// export const authDeleted = { storageState: testAuthData.deleted.path };
export const authMuted = { storageState: testAuthData.muted.path };
+195
View File
@@ -0,0 +1,195 @@
import { expect, Locator, Page, test } from '@playwright/test';
import { authDegen, authMod } from './auth/data';
import { queryGeneratedImagesReturn } from './responses/queryGeneratedImages';
import { apiResp } from './utils';
test('404', async ({ page }) => {
// test 404 page
await page.goto('/asdf');
await expect(page.getByText('page could not be found')).toBeVisible();
});
test('no login', async ({ page }) => {
// test redirect on no login
await page.goto('/user/account');
await expect(page).toHaveURL('/');
});
test.describe('examples', () => {
test.use(authMod);
test('validate mod user', async ({ page }) => {
await page.goto('/user/account');
await expect(page.getByRole('textbox', { name: 'Name' })).toHaveValue('test-mod');
});
test('chat button', async ({ page, isMobile }) => {
// picking a low impact page
await page.goto('/content/privacy');
// note - this has exposed an issue, if you click the button before "something" happens, the chat window does not show up sometimes
await page.waitForResponse(
(resp) => resp.url().includes('/api/trpc/chat.getUnreadCount') && resp.status() === 200
);
// there are two buttons due to media queries
const btn = page.getByTestId('open-chat').locator('visible=true');
await btn.click();
// for localstorage changes, we could inspect or manually set the storage here
const confirmBtn = page.getByRole('button', { name: 'Got it' });
if (await confirmBtn.isVisible()) {
await confirmBtn.click();
}
// mobile view differs
const locator = isMobile
? page.getByText('Chats', { exact: true })
: page.getByText('New Chat');
// toggle chat window and check visibility
await expect(locator).toBeVisible();
await btn.click();
await expect(locator).not.toBeVisible();
});
});
test.describe('comments', () => {
test.use(authDegen);
test('comment chain', async ({ page }) => {
const text =
'testing long comment replies. this is a test of someone saying something, and then eventually responding to it.';
// Helper function to post a comment or reply
const postComment = async (loc: Locator | Page, isReply: boolean) => {
if (isReply) {
await loc.getByRole('button', { name: 'Reply' }).click();
} else {
await loc.getByTestId('comment-form').click();
}
// TODO there is a delay here, sometimes cutting off the first letter
// await page.keyboard.type(text);
await loc.getByTestId('comment-form').locator('div').nth(2).fill(text);
// avoid race condition for clicking and awaiting response
const [response] = await Promise.all([
page.waitForResponse(
(resp) => resp.url().includes('/api/trpc/commentv2.upsert') && resp.status() === 200
),
loc.getByRole('button', { name: 'Comment' }).click(),
]);
// get the id in the response, and check the value in the relevant field
const json = await response.json();
const commentId = json?.result?.data?.json?.id;
expect(commentId).toBeGreaterThan(0);
const newLoc = page.locator(`#comment-${commentId}`);
await expect(newLoc.getByRole('paragraph')).toHaveText(text);
return newLoc;
};
await page.goto('/images/1');
// Post initial comment
const commentLoc = await postComment(page, false);
// replies
const commentLoc2 = await postComment(commentLoc, true);
const commentLoc3 = await postComment(commentLoc2, true);
const commentLoc4 = await postComment(commentLoc3, true);
const commentLoc5 = await postComment(commentLoc4, true);
await expect(commentLoc5).toBeInViewport();
});
});
test.describe('generation', () => {
test.use(authDegen);
test('mock generation', async ({ page }) => {
// override orchestrator calls with custom response
await page.route(/\/api\/trpc\/orchestrator.queryGeneratedImages(\?|$)/, async (route) => {
await route.fulfill({ json: queryGeneratedImagesReturn });
});
await page.goto('/articles/1');
await page.getByRole('button').filter({ hasText: 'Create' }).click();
const confirmBtn = page.getByRole('button', { name: 'I Confirm, Start Generating' });
if (await confirmBtn.isVisible()) {
await confirmBtn.click();
}
// this is the queue button, but i can't seem to add a data-testid without the whole thing breaking
await page.locator('div:nth-child(4) > .__mantine-ref-label').first().click();
await expect(page.getByTestId('generation-feed-list').locator('> div')).toHaveCount(19);
});
});
test.describe('error handling', () => {
test.use(authDegen);
test('error uploading model', async ({ page }) => {
await page.route('/api/trpc/tag.getAll*', async (route) => {
await route.fulfill({
json: apiResp({
items: [
{
id: 1,
name: 'stuff',
isCategory: true,
},
],
}),
});
});
await page.goto('/models/create');
// test invalid form
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.locator('form')).toContainText('Required');
await expect(page.getByText('Cannot be empty', { exact: true })).toBeVisible();
await page.getByRole('textbox', { name: 'Name' }).fill('xyz');
await page.getByText('Trained', { exact: true }).click();
await page.getByRole('searchbox', { name: 'Category' }).click();
await page.getByRole('option', { name: 'Stuff' }).click();
await page.locator('.ProseMirror').fill('qwd');
await page.getByRole('radio', { name: 'No' }).check();
await page.getByRole('checkbox', { name: 'I acknowledge that I have' }).check();
await page.route('/api/trpc/model.upsert', async (route) => {
await route.fulfill({
json: {
error: {
json: {
message: 'bad stuff.',
code: -32600,
data: {
code: 'BAD_REQUEST',
httpStatus: 400,
path: 'model.upsert',
},
},
},
},
status: 400,
});
});
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByText('Failed to save model')).toBeVisible();
await page.unroute('/api/trpc/model.upsert');
await Promise.all([
page.waitForResponse(
(resp) => resp.url().includes('/api/trpc/model.upsert') && resp.status() === 200
),
page.getByRole('button', { name: 'Next' }).click(),
]);
await expect(page.getByRole('heading', { name: 'Add version' })).toBeVisible();
});
});
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
export const apiResp = (d: any, meta?: any) => {
return {
result: {
data: {
json: d,
meta: meta ?? {},
},
},
};
};