517 lines
No EOL
18 KiB
TypeScript
517 lines
No EOL
18 KiB
TypeScript
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
|
|
}
|
|
} |