chore: update application configuration and UI components for improved styling and functionality

This commit is contained in:
Jennie Robinson Faber 2025-08-16 08:13:35 +01:00
parent 0af6b17792
commit 37ab8d7bab
54 changed files with 23293 additions and 1666 deletions

View 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
}
}