chore: update application configuration and UI components for improved styling and functionality
This commit is contained in:
parent
0af6b17792
commit
37ab8d7bab
54 changed files with 23293 additions and 1666 deletions
517
tests/e2e/conflict-resolution-utils.ts
Normal file
517
tests/e2e/conflict-resolution-utils.ts
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
import { Page } from '@playwright/test'
|
||||
|
||||
// Test data fixtures
|
||||
export const testFormData = {
|
||||
// Basic organization info
|
||||
orgName: 'Test Cooperative Solutions',
|
||||
orgType: 'Worker Cooperative',
|
||||
memberCount: '12',
|
||||
|
||||
// Core values (checkboxes)
|
||||
coreValues: ['Mutual Care', 'Transparency', 'Accountability'],
|
||||
customValues: 'We prioritize collective decision-making and equitable resource distribution.',
|
||||
|
||||
// Conflict types (checkboxes)
|
||||
conflictTypes: [
|
||||
'Interpersonal disputes between members',
|
||||
'Code of Conduct violations',
|
||||
'Work performance issues',
|
||||
'Financial disagreements'
|
||||
],
|
||||
|
||||
// Resolution approach
|
||||
approach: 'restorative',
|
||||
anonymousReporting: true,
|
||||
|
||||
// Report receivers (checkboxes)
|
||||
reportReceivers: [
|
||||
'Designated conflict resolution committee',
|
||||
'Executive Director(s)',
|
||||
'Designated staff liaison'
|
||||
],
|
||||
|
||||
// Mediator structure
|
||||
mediatorType: 'Standing committee',
|
||||
supportPeople: true,
|
||||
|
||||
// Process steps (checkboxes)
|
||||
processSteps: [
|
||||
'Initial report/complaint received',
|
||||
'Acknowledgment sent to complainant',
|
||||
'Initial assessment by designated party',
|
||||
'Informal resolution attempted',
|
||||
'Formal investigation if needed',
|
||||
'Resolution/decision reached',
|
||||
'Follow-up and monitoring'
|
||||
],
|
||||
|
||||
// Timeline
|
||||
initialResponse: 'Within 48 hours',
|
||||
resolutionTarget: '2 weeks',
|
||||
|
||||
// Available actions (checkboxes)
|
||||
availableActions: [
|
||||
'Verbal warning',
|
||||
'Written warning',
|
||||
'Required training/education',
|
||||
'Mediation facilitation',
|
||||
'Temporary suspension'
|
||||
],
|
||||
|
||||
// Documentation and confidentiality
|
||||
docLevel: 'Detailed - comprehensive documentation',
|
||||
confidentiality: 'Need-to-know basis',
|
||||
retention: '7 years',
|
||||
appealProcess: true,
|
||||
training: 'All committee members must complete conflict resolution training annually.',
|
||||
|
||||
// Implementation
|
||||
reviewSchedule: 'Annually',
|
||||
amendments: 'Consent process (no objections)',
|
||||
createdDate: '2024-03-15',
|
||||
reviewDate: '2025-03-15',
|
||||
|
||||
// Enhanced sections
|
||||
reflectionPeriod: '24-48 hours before complaint',
|
||||
customReflectionPrompts: 'Consider: What outcome would best serve the collective? How can this become a learning opportunity?',
|
||||
requireDirectAttempt: true,
|
||||
documentDirectResolution: true,
|
||||
|
||||
// Communication channels (checkboxes)
|
||||
communicationChannels: [
|
||||
'Asynchronous text (Slack, email)',
|
||||
'Synchronous text (planned chat session)',
|
||||
'Audio call or huddle',
|
||||
'Video conference'
|
||||
],
|
||||
|
||||
// Internal advisor
|
||||
internalAdvisorType: 'Committee-designated advisor',
|
||||
staffLiaison: 'Operations Coordinator',
|
||||
boardChairRole: 'First contact for ED complaints',
|
||||
|
||||
// Formal complaints
|
||||
formalComplaintElements: [
|
||||
'The complainant\'s name',
|
||||
'The respondent\'s name',
|
||||
'Detailed information about the issue (what, where, when)',
|
||||
'Details of all prior resolution attempts',
|
||||
'The specific outcome(s) the complainant is seeking'
|
||||
],
|
||||
formalAcknowledgmentTime: 'Within 48 hours',
|
||||
formalReviewTime: '1 month',
|
||||
requireExternalAdvice: true,
|
||||
|
||||
// Settlement
|
||||
requireMinutesOfSettlement: true,
|
||||
settlementConfidentiality: 'Restricted to parties and advisors',
|
||||
conflictFileRetention: '7 years',
|
||||
|
||||
// External resources
|
||||
includeHumanRights: true,
|
||||
additionalResources: 'Local Community Mediation Center: (555) 123-4567\nWorker Cooperative Legal Aid: www.coop-legal.org',
|
||||
acknowledgments: 'This policy was developed with input from the Media Arts Network of Ontario and local cooperative development specialists.',
|
||||
|
||||
// Special circumstances (checkboxes)
|
||||
specialCircumstances: [
|
||||
'Include immediate removal protocol for safety threats',
|
||||
'Reference external reporting options (Human Rights Tribunal, etc.)',
|
||||
'Include anti-retaliation provisions'
|
||||
]
|
||||
}
|
||||
|
||||
// Utility functions for form interaction
|
||||
export class ConflictResolutionFormHelper {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/templates/conflict-resolution-framework')
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
async fillBasicInfo(data: typeof testFormData) {
|
||||
// Fill text inputs
|
||||
await this.page.fill('input[placeholder*="organization name"]', data.orgName)
|
||||
|
||||
// Handle USelect component for organization type
|
||||
const orgTypeButton = this.page.locator('button:has-text("Select organization type"), [role="combobox"]:has-text("Select organization type")').first()
|
||||
await orgTypeButton.click()
|
||||
await this.page.locator(`text="${data.orgType}"`).click()
|
||||
|
||||
await this.page.fill('input[type="number"]', data.memberCount)
|
||||
}
|
||||
|
||||
async selectCheckboxes(sectionSelector: string, items: string[]) {
|
||||
for (const item of items) {
|
||||
const checkbox = this.page.locator(`${sectionSelector} label:has-text("${item}") input[type="checkbox"]`)
|
||||
await checkbox.check()
|
||||
}
|
||||
}
|
||||
|
||||
async fillCoreValues(data: typeof testFormData) {
|
||||
// Enable values section if it has a toggle
|
||||
const valuesToggle = this.page.locator('.toggle:near(:text("Guiding Principles"))')
|
||||
if (await valuesToggle.isVisible()) {
|
||||
await valuesToggle.click()
|
||||
}
|
||||
|
||||
// Select core values checkboxes
|
||||
for (const value of data.coreValues) {
|
||||
await this.page.locator(`label:has-text("${value}") input[type="checkbox"]`).check()
|
||||
}
|
||||
|
||||
// Fill custom values textarea
|
||||
await this.page.fill('textarea[placeholder*="values"], textarea[placeholder*="principles"]', data.customValues)
|
||||
}
|
||||
|
||||
async fillConflictTypes(data: typeof testFormData) {
|
||||
for (const type of data.conflictTypes) {
|
||||
await this.page.locator(`label:has-text("${type}") input[type="checkbox"]`).check()
|
||||
}
|
||||
}
|
||||
|
||||
async fillApproach(data: typeof testFormData) {
|
||||
await this.page.locator(`input[value="${data.approach}"]`).check()
|
||||
|
||||
if (data.anonymousReporting) {
|
||||
await this.page.locator('input[id*="anonymous"], label:has-text("anonymous") input').check()
|
||||
}
|
||||
}
|
||||
|
||||
async fillReportReceivers(data: typeof testFormData) {
|
||||
for (const receiver of data.reportReceivers) {
|
||||
await this.page.locator(`label:has-text("${receiver}") input[type="checkbox"]`).check()
|
||||
}
|
||||
}
|
||||
|
||||
async fillMediatorStructure(data: typeof testFormData) {
|
||||
await this.page.selectOption('select:has-text("mediator"), select[placeholder*="mediator"]', data.mediatorType)
|
||||
|
||||
if (data.supportPeople) {
|
||||
await this.page.locator('label:has-text("support people"), label:has-text("Support people") input').check()
|
||||
}
|
||||
}
|
||||
|
||||
async fillProcessSteps(data: typeof testFormData) {
|
||||
for (const step of data.processSteps) {
|
||||
await this.page.locator(`label:has-text("${step}") input[type="checkbox"]`).check()
|
||||
}
|
||||
}
|
||||
|
||||
async fillTimeline(data: typeof testFormData) {
|
||||
await this.page.selectOption('select:has-text("response"), select[placeholder*="response"]', data.initialResponse)
|
||||
await this.page.selectOption('select:has-text("resolution"), select[placeholder*="target"]', data.resolutionTarget)
|
||||
}
|
||||
|
||||
async fillAvailableActions(data: typeof testFormData) {
|
||||
for (const action of data.availableActions) {
|
||||
await this.page.locator(`label:has-text("${action}") input[type="checkbox"]`).check()
|
||||
}
|
||||
}
|
||||
|
||||
async fillDocumentation(data: typeof testFormData) {
|
||||
await this.page.selectOption('select:has-text("documentation"), select[placeholder*="level"]', data.docLevel)
|
||||
await this.page.selectOption('select:has-text("confidentiality"),' , data.confidentiality)
|
||||
await this.page.selectOption('select:has-text("retention"),' , data.retention)
|
||||
|
||||
if (data.appealProcess) {
|
||||
await this.page.locator('label:has-text("appeal") input[type="checkbox"]').check()
|
||||
}
|
||||
|
||||
await this.page.fill('textarea[placeholder*="training"]', data.training)
|
||||
}
|
||||
|
||||
async fillImplementation(data: typeof testFormData) {
|
||||
await this.page.selectOption('select:has-text("review"), select[placeholder*="schedule"]', data.reviewSchedule)
|
||||
await this.page.selectOption('select:has-text("amendment"),' , data.amendments)
|
||||
await this.page.fill('input[type="date"]:first-of-type', data.createdDate)
|
||||
await this.page.fill('input[type="date"]:last-of-type', data.reviewDate)
|
||||
}
|
||||
|
||||
async fillEnhancedSections(data: typeof testFormData) {
|
||||
// Reflection section
|
||||
const reflectionToggle = this.page.locator('.toggle:near(:text("Reflection"))')
|
||||
if (await reflectionToggle.isVisible()) {
|
||||
await reflectionToggle.click()
|
||||
}
|
||||
|
||||
await this.page.selectOption('select[placeholder*="reflection"]', data.reflectionPeriod)
|
||||
await this.page.fill('textarea[placeholder*="reflection"]', data.customReflectionPrompts)
|
||||
|
||||
// Direct resolution section
|
||||
const directToggle = this.page.locator('.toggle:near(:text("Direct Resolution"))')
|
||||
if (await directToggle.isVisible()) {
|
||||
await directToggle.click()
|
||||
}
|
||||
|
||||
for (const channel of data.communicationChannels) {
|
||||
await this.page.locator(`label:has-text("${channel}") input[type="checkbox"]`).check()
|
||||
}
|
||||
|
||||
if (data.requireDirectAttempt) {
|
||||
await this.page.locator('label:has-text("require direct") input').check()
|
||||
}
|
||||
|
||||
if (data.documentDirectResolution) {
|
||||
await this.page.locator('label:has-text("written record") input').check()
|
||||
}
|
||||
|
||||
// RCP section
|
||||
await this.page.selectOption('select[placeholder*="internal advisor"]', data.internalAdvisorType)
|
||||
await this.page.fill('input[placeholder*="staff liaison"]', data.staffLiaison)
|
||||
await this.page.selectOption('select[placeholder*="board chair"]', data.boardChairRole)
|
||||
|
||||
// Formal complaints
|
||||
for (const element of data.formalComplaintElements) {
|
||||
await this.page.locator(`label:has-text("${element}") input[type="checkbox"]`).check()
|
||||
}
|
||||
|
||||
await this.page.selectOption('select[placeholder*="acknowledgment"]', data.formalAcknowledgmentTime)
|
||||
await this.page.selectOption('select[placeholder*="review time"]', data.formalReviewTime)
|
||||
|
||||
if (data.requireExternalAdvice) {
|
||||
await this.page.locator('label:has-text("external") input').check()
|
||||
}
|
||||
|
||||
// Settlement
|
||||
if (data.requireMinutesOfSettlement) {
|
||||
await this.page.locator('label:has-text("Minutes of Settlement") input').check()
|
||||
}
|
||||
|
||||
await this.page.selectOption('select[placeholder*="confidentiality"]', data.settlementConfidentiality)
|
||||
await this.page.selectOption('select[placeholder*="retention"]', data.conflictFileRetention)
|
||||
|
||||
// External resources
|
||||
const resourcesToggle = this.page.locator('.toggle:near(:text("External Resources"))')
|
||||
if (await resourcesToggle.isVisible()) {
|
||||
await resourcesToggle.click()
|
||||
}
|
||||
|
||||
if (data.includeHumanRights) {
|
||||
await this.page.locator('label:has-text("Human Rights") input').check()
|
||||
}
|
||||
|
||||
await this.page.fill('textarea[placeholder*="external resources"]', data.additionalResources)
|
||||
await this.page.fill('textarea[placeholder*="acknowledgment"]', data.acknowledgments)
|
||||
|
||||
// Special circumstances
|
||||
const specialToggle = this.page.locator('.toggle:near(:text("Special"))')
|
||||
if (await specialToggle.isVisible()) {
|
||||
await specialToggle.click()
|
||||
}
|
||||
|
||||
for (const circumstance of data.specialCircumstances) {
|
||||
await this.page.locator(`label:has-text("${circumstance}") input[type="checkbox"]`).check()
|
||||
}
|
||||
}
|
||||
|
||||
async downloadMarkdown(): Promise<string> {
|
||||
// Click download markdown button
|
||||
const downloadPromise = this.page.waitForDownload()
|
||||
await this.page.locator('button:has-text("MARKDOWN"), button:has-text("Download Policy")').click()
|
||||
const download = await downloadPromise
|
||||
|
||||
// Read downloaded file content
|
||||
const stream = await download.createReadStream()
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', chunk => chunks.push(chunk))
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
|
||||
stream.on('error', reject)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Markdown parsing and validation utilities
|
||||
export class MarkdownValidator {
|
||||
constructor(private markdown: string) {}
|
||||
|
||||
validateOrganizationInfo(expectedData: typeof testFormData) {
|
||||
const errors: string[] = []
|
||||
|
||||
// Check organization name in title
|
||||
if (!this.markdown.includes(`# ${expectedData.orgName} Conflict Resolution Policy`)) {
|
||||
errors.push(`Title should contain "${expectedData.orgName}"`)
|
||||
}
|
||||
|
||||
// Check organization type context
|
||||
const hasCooperativeReferences = expectedData.orgType.includes('Cooperative')
|
||||
const hasMembers = this.markdown.includes('members')
|
||||
|
||||
if (hasCooperativeReferences && !hasMembers) {
|
||||
errors.push('Cooperative org type should reference members')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
validateCoreValues(expectedData: typeof testFormData) {
|
||||
const errors: string[] = []
|
||||
|
||||
// Check each core value appears
|
||||
for (const value of expectedData.coreValues) {
|
||||
if (!this.markdown.includes(value)) {
|
||||
errors.push(`Core value "${value}" not found in markdown`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check custom values text
|
||||
if (expectedData.customValues && !this.markdown.includes(expectedData.customValues)) {
|
||||
errors.push('Custom values text not found in markdown')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
validateConflictTypes(expectedData: typeof testFormData) {
|
||||
const errors: string[] = []
|
||||
|
||||
// Find the conflict types table
|
||||
const tableMatch = this.markdown.match(/\| \*\*Who Can File\*\* \| \*\*Type of Complaint\*\* \| \*\*Policy Reference\*\* \| \*\*Additional Notes\*\* \|(.*?)(?=\n\n|\n#)/s)
|
||||
|
||||
if (!tableMatch) {
|
||||
errors.push('Conflict types table not found')
|
||||
return errors
|
||||
}
|
||||
|
||||
const tableContent = tableMatch[1]
|
||||
|
||||
// Check each selected conflict type appears in table
|
||||
for (const type of expectedData.conflictTypes) {
|
||||
if (!tableContent.includes(type)) {
|
||||
errors.push(`Conflict type "${type}" not found in table`)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
validateProcessSteps(expectedData: typeof testFormData) {
|
||||
const errors: string[] = []
|
||||
|
||||
for (const step of expectedData.processSteps) {
|
||||
if (!this.markdown.includes(step)) {
|
||||
errors.push(`Process step "${step}" not found`)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
validateTimeline(expectedData: typeof testFormData) {
|
||||
const errors: string[] = []
|
||||
|
||||
// Check response time
|
||||
if (!this.markdown.includes(expectedData.initialResponse.toLowerCase())) {
|
||||
errors.push(`Initial response time "${expectedData.initialResponse}" not found`)
|
||||
}
|
||||
|
||||
// Check resolution target
|
||||
if (!this.markdown.includes(expectedData.resolutionTarget.toLowerCase())) {
|
||||
errors.push(`Resolution target "${expectedData.resolutionTarget}" not found`)
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
validateAvailableActions(expectedData: typeof testFormData) {
|
||||
const errors: string[] = []
|
||||
|
||||
for (const action of expectedData.availableActions) {
|
||||
if (!this.markdown.includes(action)) {
|
||||
errors.push(`Available action "${action}" not found`)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
validateEnhancedSections(expectedData: typeof testFormData) {
|
||||
const errors: string[] = []
|
||||
|
||||
// Check reflection section
|
||||
if (expectedData.customReflectionPrompts && !this.markdown.includes(expectedData.customReflectionPrompts)) {
|
||||
errors.push('Custom reflection prompts not found')
|
||||
}
|
||||
|
||||
// Check communication channels
|
||||
for (const channel of expectedData.communicationChannels) {
|
||||
if (!this.markdown.includes(channel)) {
|
||||
errors.push(`Communication channel "${channel}" not found`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check staff liaison
|
||||
if (expectedData.staffLiaison && !this.markdown.includes(expectedData.staffLiaison)) {
|
||||
errors.push(`Staff liaison "${expectedData.staffLiaison}" not found`)
|
||||
}
|
||||
|
||||
// Check formal complaint elements
|
||||
for (const element of expectedData.formalComplaintElements) {
|
||||
if (!this.markdown.includes(element)) {
|
||||
errors.push(`Formal complaint element "${element}" not found`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check external resources
|
||||
if (expectedData.additionalResources && !this.markdown.includes(expectedData.additionalResources)) {
|
||||
errors.push('Additional resources not found')
|
||||
}
|
||||
|
||||
if (expectedData.acknowledgments && !this.markdown.includes(expectedData.acknowledgments)) {
|
||||
errors.push('Acknowledgments not found')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
validateDates(expectedData: typeof testFormData) {
|
||||
const errors: string[] = []
|
||||
|
||||
if (expectedData.createdDate && !this.markdown.includes(expectedData.createdDate)) {
|
||||
errors.push(`Created date "${expectedData.createdDate}" not found`)
|
||||
}
|
||||
|
||||
if (expectedData.reviewDate && !this.markdown.includes(expectedData.reviewDate)) {
|
||||
errors.push(`Review date "${expectedData.reviewDate}" not found`)
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
validateLanguageQuality() {
|
||||
const errors: string[] = []
|
||||
|
||||
// Check for common language issues we fixed
|
||||
if (this.markdown.includes("'s's")) {
|
||||
errors.push('Incorrect possessive form found ("\'s\'s")')
|
||||
}
|
||||
|
||||
if (this.markdown.includes('within within')) {
|
||||
errors.push('Redundant "within within" found')
|
||||
}
|
||||
|
||||
if (this.markdown.includes('members members')) {
|
||||
errors.push('Redundant word repetition found')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
validateAll(expectedData: typeof testFormData) {
|
||||
const allErrors = [
|
||||
...this.validateOrganizationInfo(expectedData),
|
||||
...this.validateCoreValues(expectedData),
|
||||
...this.validateConflictTypes(expectedData),
|
||||
...this.validateProcessSteps(expectedData),
|
||||
...this.validateTimeline(expectedData),
|
||||
...this.validateAvailableActions(expectedData),
|
||||
...this.validateEnhancedSections(expectedData),
|
||||
...this.validateDates(expectedData),
|
||||
...this.validateLanguageQuality()
|
||||
]
|
||||
|
||||
return allErrors
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue