Skip to content
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
VITE_API_URL=https://physics-api-cn.turtlesim.com
VITE_STATIC_URL=https://physics-lab.oss-cn-hongkong.aliyuncs.com
VITE_DEMOCRACY_WALL_MODE=off

# build之后API请求发送的路径
# The path where API requests are sent after build
271 changes: 271 additions & 0 deletions docs/democracy-wall-backend-contract.md

Large diffs are not rendered by default.

342 changes: 342 additions & 0 deletions scripts/tests/democracy-wall-demo.spec.ts

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions scripts/tests/democracy-wall-edit-lock.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { test, expect } from './fixtures'
import { injectLoginStateWithoutNavigation } from './test-helpers'

test.beforeEach(async ({ page }) => {
await injectLoginStateWithoutNavigation(page)
await page.goto('/#/d?demo=1&admin=1')
})

test('发布者投票前可重复编辑,跨过开始时间后服务层拒绝修改', async ({ page }) => {
const result = await page.evaluate(async () => {
const path = '/src/services/democracyWall.ts'
const service = await import(path)
const plan = {
StartAt: new Date(Date.now() + 60000).toISOString(),
FinishAt: new Date(Date.now() + 3600000).toISOString(),
Options: ['赞成', '反对'],
Multiple: false,
}
const matter = await service.submitDemocracyMatter({
subject: '编辑时间边界测试',
description: '用于验证发起后到投票开始之间的编辑权限。',
anonymous: false,
kind: 'public',
participation: 'Vote',
votePlan: plan,
})
const input = {
matterId: matter.ID,
expectedRevision: String(matter.UpdateDate),
subject: '投票开始前第一次修改',
description: '这里是第一次修改后的完整事务正文。',
participation: 'Vote',
votePlan: plan,
}
await service.updateDemocracyMatter(input)
await service.updateDemocracyMatter({ ...input, subject: '投票开始前第二次修改' })
const clock = Date.now
Date.now = () => new Date(plan.StartAt).getTime()
let error = ''
try {
await service.updateDemocracyMatter({ ...input, subject: '投票后禁止保存内容' })
} catch (e) {
error = (e as Error).message
} finally {
Date.now = clock
}
return { error, subject: (await service.fetchDemocracyMatter(matter.ID)).summary.Subject }
})
expect(result).toEqual({ error: 'permission-denied', subject: '投票开始前第二次修改' })
})

test('编辑页面跨过投票开始时间自动禁用,开发权限可修改正文', async ({ page }) => {
const id = await page.evaluate(async () => {
const path = '/src/services/democracyWall.ts'
const service = await import(path)
const matter = await service.submitDemocracyMatter({
subject: '编辑页自动锁定测试',
description: '用于验证编辑页面跨越投票开始时间后自动锁定。',
anonymous: false,
kind: 'public',
participation: 'Vote',
votePlan: {
StartAt: new Date(Date.now() + 10000).toISOString(),
FinishAt: new Date(Date.now() + 3600000).toISOString(),
Options: ['同意', '不同意'],
Multiple: false,
},
})
return matter.ID
})
await page.goto(`/#/d/new?edit=${id}&demo=1&admin=1`)
const title = page.getByRole('textbox', { name: '标题', exact: true })
await expect(title).toBeEnabled()
await expect(title).toBeDisabled({ timeout: 15000 })
await page.goto(`/#/d/new?edit=${id}&demo=1&admin=1&developer=1`)
await expect(title).toBeEnabled()
await title.fill('开发权限修订后的正文标题')
await expect(page.locator('input[type="datetime-local"]').first()).toBeDisabled()
await page.getByRole('button', { name: '发布', exact: true }).click()
await expect(page.getByRole('heading', { name: '开发权限修订后的正文标题' })).toBeVisible()
})
279 changes: 279 additions & 0 deletions scripts/tests/democracy-wall-permissions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import type { Page } from '@playwright/test'
import { expect, test } from './fixtures'
import { injectLoginStateWithoutNavigation, waitForPageReady } from './test-helpers'

interface PermissionCase {
alias?: string
canPublish: boolean
login: boolean
name: string
permissions: {
CanContribute: boolean
CanInitiate: boolean
CanModerate: boolean
CanSuggest: boolean
CanTransition: boolean
}
verification?: string
}

const matterSummary = {
ID: '66a84559744ed757b46f8917',
Tags: ['民主墙', '公开卷宗'],
Type: 0,
User: {
ID: '',
Nickname: '匿名调查员',
Avatar: 0,
AvatarRegion: 0,
Signature: '',
Decoration: 0,
Verification: undefined,
},
Image: 0,
ImageRegion: 0,
Price: 0,
Stars: 0,
Editor: null,
Visits: 0,
ModelID: null,
Remixes: 0,
Subject: '权限兼容性测试事务',
Version: 1,
Category: 'Discussion',
Comments: 0,
Language: 'Chinese',
Supports: 0,
Coauthors: [],
Popularity: 0,
UpdateDate: 0,
Visibility: 0,
SortingDate: 0,
CreationDate: 0,
Multilingual: false,
Description: ['用于验证不同账号权限下的详情页写入口。'],
}

const permissionCases: PermissionCase[] = [
{
canPublish: false,
login: false,
name: '游客',
permissions: {
CanContribute: false,
CanInitiate: false,
CanModerate: false,
CanSuggest: false,
CanTransition: false,
},
},
{
alias: '普通用户化名',
canPublish: true,
login: true,
name: '普通用户',
permissions: {
CanContribute: true,
CanInitiate: false,
CanModerate: false,
CanSuggest: false,
CanTransition: false,
},
verification: 'user',
},
{
alias: '资深用户化名',
canPublish: true,
login: true,
name: 'Oldtimer',
permissions: {
CanContribute: true,
CanInitiate: false,
CanModerate: false,
CanSuggest: true,
CanTransition: false,
},
verification: 'Oldtimer',
},
{
alias: '编辑化名',
canPublish: true,
login: true,
name: '认证编辑',
permissions: {
CanContribute: true,
CanInitiate: true,
CanModerate: false,
CanSuggest: false,
CanTransition: false,
},
verification: 'Editor',
},
{
alias: '管理化名',
canPublish: true,
login: true,
name: '管理员',
permissions: {
CanContribute: true,
CanInitiate: true,
CanModerate: true,
CanSuggest: true,
CanTransition: true,
},
verification: 'Administrator',
},
{
alias: '受限账号',
canPublish: false,
login: true,
name: '被禁发管理员',
permissions: {
CanContribute: true,
CanInitiate: true,
CanModerate: true,
CanSuggest: true,
CanTransition: true,
},
verification: 'Administrator',
},
]

async function mockPermissionApis(page: Page, account: PermissionCase) {
await page.route('**/api/Democracy/GetContext', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
Status: 200,
Message: '',
Data: {
ApiVersion: 1,
Permissions: account.permissions,
Profile: account.login
? { Alias: account.alias, CanPublish: account.canPublish, ModifiedAt: '' }
: null,
},
}),
}),
)
await page.route('**/api/Democracy/QueryMatters', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ Status: 200, Message: '', Data: { Entries: [] } }),
}),
)
await page.route('**/api/Democracy/GetMatter', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
Status: 200,
Message: '',
Data: {
CanEdit: account.login,
Kind: 'Public',
Mode: 'Formal',
Participation: 'Consultation',
Revision: '1',
Stage: 'Questions',
Summary: matterSummary,
},
}),
}),
)
await page.route('**/api/Democracy/QueryContributions', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ Status: 200, Message: '', Data: { Entries: [] } }),
}),
)
await page.route('**/api/Democracy/QueryAudit', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ Status: 200, Message: '', Data: { Entries: [] } }),
}),
)
await page.route('**/api/Democracy/QueryInvestigationTeam', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ Status: 200, Message: '', Data: { Entries: [] } }),
}),
)
await page.route('**/api/Users/Authenticate', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
Status: 200,
Message: '',
Data: { Activities: [], Statistic: { Activities: [], ID: 'permission-statistic' } },
}),
}),
)
}

async function injectGuestPreferences(page: Page) {
await page.addInitScript(() => {
const now = Date.now()
localStorage.setItem(
'userConfig',
JSON.stringify({
value: { language: 'zh', languageManuallySelected: true },
time: now,
maxAgeMs: 30 * 24 * 60 * 60 * 1000,
}),
)
localStorage.setItem(
'cookieConsent',
JSON.stringify({ value: true, time: now, maxAgeMs: 365 * 24 * 60 * 60 * 1000 }),
)
})
}

function expectedWriteCount(account: PermissionCase, permission = true) {
return account.login && account.canPublish && permission ? 1 : 0
}

async function prepareAccount(page: Page, account: PermissionCase) {
if (account.login) {
await injectLoginStateWithoutNavigation(page, { verification: account.verification })
return
}
await injectGuestPreferences(page)
}

async function assertWallPermissions(page: Page, account: PermissionCase) {
await expect(page.getByRole('link', { name: '匿名提议' })).toHaveCount(
expectedWriteCount(account, account.permissions.CanSuggest),
)
await expect(page.getByRole('link', { name: '发起事务' })).toHaveCount(
expectedWriteCount(account, account.permissions.CanInitiate),
)
}

async function assertDetailPermissions(page: Page, account: PermissionCase) {
await expect(page.locator('.comment-composer')).toHaveCount(
expectedWriteCount(account, account.permissions.CanContribute),
)
await expect(page.getByRole('button', { name: '举报事务' })).toHaveCount(
expectedWriteCount(account),
)
await expect(page.getByRole('button', { name: '修改事务' })).toHaveCount(
expectedWriteCount(account),
)
await expect(page.getByRole('heading', { name: '事务管理与审计' })).toHaveCount(
expectedWriteCount(account, false),
)
}

for (const account of permissionCases) {
test(`${account.name}的民主墙写入口与服务端权限一致`, async ({ page }) => {
await prepareAccount(page, account)
await mockPermissionApis(page, account)
await page.goto('/#/d')
await waitForPageReady(page)
await assertWallPermissions(page, account)

await page.goto('/#/d/matter/66a84559744ed757b46f8917')
await expect(page.getByRole('heading', { name: '事务详情' })).toBeVisible()
await assertDetailPermissions(page, account)
})
}
Loading
Loading