2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
e9c570a821 Add comprehensive GitHub Copilot instructions with validated build processes
Co-authored-by: snow-jallen <42281341+snow-jallen@users.noreply.github.com>
2025-09-10 18:09:06 +00:00
copilot-swe-agent[bot]
3f44e54c9b Initial plan 2025-09-10 17:54:50 +00:00
9 changed files with 184 additions and 688 deletions

152
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,152 @@
# Canvas Management Application
Canvas Management is a Next.js web application that provides a user interface for managing Canvas LMS courses, modules, assignments, quizzes, and pages. It features real-time file synchronization through WebSocket connections and supports Docker containerization.
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
## Working Effectively
### Bootstrap, Build, and Test the Repository
- Install pnpm globally: `npm install -g pnpm`
- Install dependencies: `pnpm install --config.confirmModulesPurge=false` -- takes 25 seconds. NEVER CANCEL. Set timeout to 60+ seconds.
- Build the application: `pnpm run build` -- takes 47 seconds. NEVER CANCEL. Set timeout to 90+ minutes for CI environments.
- Run tests: `pnpm run test` -- takes 8 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
- Run linting: `pnpm run lint` -- takes 13 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
### Environment Setup
- Copy `.env.test` to `.env.local` for local development
- Set required environment variables:
- `STORAGE_DIRECTORY="./temp/storage"` - Directory for course data storage
- `NEXT_PUBLIC_ENABLE_FILE_SYNC=true` - Enable file synchronization features
- `CANVAS_TOKEN="your_canvas_api_token"` - Canvas API token (optional for local development)
- Create storage directory: `mkdir -p temp/storage`
- Create simple globalSettings.yml: `echo "courses: []" > globalSettings.yml`
### Run the Development Server
- **Development server without WebSocket**: `pnpm run devNoSocket` -- starts in 1.5 seconds
- **Development server with WebSocket**: `pnpm run dev` -- runs `pnpm install` then starts WebSocket server with Next.js
- Access the application at: `http://localhost:3000`
- WebSocket server enables real-time file watching and synchronization
### Run Production Build
- **Production server**: `pnpm run start` -- starts production WebSocket server with Next.js
- Requires completing the build step first
### Docker Development
- **Local Docker build**: `./build.sh` -- takes 5+ minutes. NEVER CANCEL. Set timeout to 120+ minutes.
- **Development with Docker**: Use `run.sh` or `docker-compose.dev.yml`
- **Production with Docker**: Use `docker-compose.yml`
- Note: Docker builds may fail in sandboxed environments due to certificate issues
## Validation
### Manual Testing Scenarios
- ALWAYS run through complete end-to-end scenarios after making changes
- Start the development server and verify the main page loads with "Add New Course" and "Add Existing Course" buttons
- Test course creation workflow (will fail without valid Canvas API token, which is expected)
- Verify WebSocket connectivity shows "Socket connected successfully" in browser console
- Check that file system watching works when NEXT_PUBLIC_ENABLE_FILE_SYNC=true
### Required Validation Steps
- Always run `pnpm run lint` before committing changes
- Always run `pnpm run test` to ensure tests pass
- Always run `pnpm run build` to verify production build works
- Test both `devNoSocket` and `dev` modes to ensure WebSocket functionality works
## Common Tasks
### Build System Requirements
- **Node.js**: Version 20+ (validated with v20.19.5)
- **Package Manager**: pnpm v10+ (install with `npm install -g pnpm`)
- **Build Tool**: Next.js 15.3.5 with React 19
### Technology Stack
- **Frontend**: Next.js 15.3.5, React 19, TypeScript 5.8.3
- **Styling**: Tailwind CSS 4.1.11
- **Testing**: Vitest 3.2.4 with @testing-library/react
- **Linting**: ESLint 9.31.0 with Next.js and TypeScript configs
- **Real-time**: WebSocket with Socket.IO for file watching
- **API**: tRPC for type-safe API calls
- **Canvas Integration**: Axios for Canvas LMS API communication
### Key Project Structure
```
src/
├── app/ # Next.js app router pages
├── components/ # Reusable React components
├── features/ # Feature-specific code (local file handling, Canvas API)
├── services/ # Utility services and API helpers
└── websocket.js # WebSocket server for file watching
```
### Development Workflow Tips
- Use `pnpm dev` for full development with file watching
- Use `pnpm devNoSocket` for faster startup when WebSocket features not needed
- Monitor console for WebSocket connection status and Canvas API errors
- Canvas API errors are expected without valid CANVAS_TOKEN
- File sync requires NEXT_PUBLIC_ENABLE_FILE_SYNC=true
### Storage Configuration
- Course data stored in markdown files within storage directory
- `globalSettings.yml` controls which courses appear in UI
- Each course requires settings.yml file in its directory
- Images supported via volume mounts to `/app/public/images/`
### Frequent Command Outputs
#### Repository Root Structure
```
.
├── README.md
├── package.json
├── pnpm-lock.yaml
├── Dockerfile
├── docker-compose.yml
├── docker-compose.dev.yml
├── build.sh
├── run.sh
├── globalSettings.yml
├── eslint.config.mjs
├── vitest.config.ts
├── tsconfig.json
├── tailwind.config.ts
├── next.config.mjs
├── postcss.config.mjs
└── src/
```
#### Key Package Scripts
```json
{
"dev": "pnpm install --config.confirmModulesPurge=false && node src/websocket.js",
"devNoSocket": "next dev",
"build": "next build",
"start": "NODE_ENV=production node src/websocket.js",
"lint": "eslint . --config eslint.config.mjs && tsc && next lint",
"test": "vitest"
}
```
## Critical Timing Information
- **NEVER CANCEL** any build or test commands - wait for completion
- **Dependency Installation**: ~25 seconds (set 60+ second timeout)
- **Production Build**: ~47 seconds (set 90+ minute timeout for CI)
- **Test Suite**: ~8 seconds (set 30+ second timeout)
- **Linting**: ~13 seconds (set 30+ second timeout)
- **Development Server Startup**: ~1.5 seconds
- **Docker Build**: 5+ minutes locally (set 120+ minute timeout)
## Error Handling
### Expected Errors During Development
- Canvas API connection errors without valid CANVAS_TOKEN
- Socket.IO 404 errors when running devNoSocket mode
- Docker build certificate issues in sandboxed environments
- Course settings file not found errors with empty storage directory
### Troubleshooting
- If WebSocket connection fails, check NEXT_PUBLIC_ENABLE_FILE_SYNC environment variable
- If build fails, ensure pnpm is installed globally
- If tests fail with storage errors, check STORAGE_DIRECTORY environment variable
- For Canvas integration, set valid CANVAS_TOKEN environment variable

View File

@@ -1,31 +0,0 @@
name: Deploy to Docker Hub
on:
push:
branches: [ main, development, staging ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract branch name
shell: bash
run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- name: Build and push Docker image
run: |
chmod +x ./build.sh
./build.sh -t -p -b "$BRANCH_NAME"

View File

@@ -3,12 +3,11 @@
MAJOR_VERSION="3"
MINOR_VERSION="0"
VERSION="$MAJOR_VERSION.$MINOR_VERSION"
BRANCH=""
TAG_FLAG=false
PUSH_FLAG=false
while getopts ":tpb:" opt; do
while getopts ":tp" opt; do
case ${opt} in
t)
TAG_FLAG=true
@@ -16,12 +15,9 @@ while getopts ":tpb:" opt; do
p)
PUSH_FLAG=true
;;
b)
BRANCH="$OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
echo "Usage: $0 [-t] [-p] [-b branch]"
echo "Usage: $0 [-t] [-p]"
exit 1
;;
esac
@@ -33,54 +29,17 @@ docker build -t canvas_management:$VERSION .
if [ "$TAG_FLAG" = true ]; then
echo "Tagging images..."
if [ -n "$BRANCH" ]; then
# Branch-specific tags
echo "alexmickelson/canvas_management:$VERSION-$BRANCH"
echo "alexmickelson/canvas_management:$MAJOR_VERSION-$BRANCH"
echo "alexmickelson/canvas_management:latest-$BRANCH"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$VERSION-$BRANCH"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$MAJOR_VERSION-$BRANCH"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:latest-$BRANCH
# Only create non-branch tags if branch is "main"
if [ "$BRANCH" = "main" ]; then
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$VERSION"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$MAJOR_VERSION"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:latest
fi
else
# No branch specified - create standard tags (for local development)
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$VERSION"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:"$MAJOR_VERSION"
docker image tag canvas_management:"$VERSION" alexmickelson/canvas_management:latest
fi
docker image tag canvas_management:latest alexmickelson/canvas_management:latest
fi
if [ "$PUSH_FLAG" = true ]; then
echo "Pushing images..."
if [ -n "$BRANCH" ]; then
# Push branch-specific tags
echo "alexmickelson/canvas_management:$VERSION-$BRANCH"
echo "alexmickelson/canvas_management:$MAJOR_VERSION-$BRANCH"
echo "alexmickelson/canvas_management:latest-$BRANCH"
docker push alexmickelson/canvas_management:"$VERSION-$BRANCH"
docker push alexmickelson/canvas_management:"$MAJOR_VERSION-$BRANCH"
docker push alexmickelson/canvas_management:latest-$BRANCH
# Only push non-branch tags if branch is "main"
if [ "$BRANCH" = "main" ]; then
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
@@ -89,50 +48,18 @@ if [ "$PUSH_FLAG" = true ]; then
docker push alexmickelson/canvas_management:"$MAJOR_VERSION"
docker push alexmickelson/canvas_management:latest
fi
else
# No branch specified - push standard tags (for local development)
echo "alexmickelson/canvas_management:$VERSION"
echo "alexmickelson/canvas_management:$MAJOR_VERSION"
echo "alexmickelson/canvas_management:latest"
docker push alexmickelson/canvas_management:"$VERSION"
docker push alexmickelson/canvas_management:"$MAJOR_VERSION"
docker push alexmickelson/canvas_management:latest
fi
fi
if [ "$TAG_FLAG" = false ] && [ "$PUSH_FLAG" = false ]; then
echo ""
echo "Build complete."
echo "To tag, run with -t flag."
echo "To push, run with -p flag."
echo "To build for a specific branch, use -b branch_name flag."
echo "Or manually run:"
echo ""
if [ -n "$BRANCH" ]; then
echo "# Branch-specific tags:"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$VERSION-$BRANCH"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$MAJOR_VERSION-$BRANCH"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:latest-$BRANCH"
echo "docker push alexmickelson/canvas_management:$VERSION-$BRANCH"
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION-$BRANCH"
echo "docker push alexmickelson/canvas_management:latest-$BRANCH"
if [ "$BRANCH" = "main" ]; then
echo ""
echo "# Main branch also gets standard tags:"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$VERSION"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:latest"
echo "docker image tag canvas_management:latest alexmickelson/canvas_management:latest"
echo "docker push alexmickelson/canvas_management:$VERSION"
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker push alexmickelson/canvas_management:latest"
fi
else
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$VERSION"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker image tag canvas_management:$VERSION alexmickelson/canvas_management:latest"
echo "docker push alexmickelson/canvas_management:$VERSION"
echo "docker push alexmickelson/canvas_management:$MAJOR_VERSION"
echo "docker push alexmickelson/canvas_management:latest"
fi
fi

View File

@@ -19,5 +19,3 @@ courses:
name: Jonathan UX
- path: ./1400/2025_spring_alex/modules/
name: 1400-spring
- path: ./1420/2024-fall/Modules/
name: 1420_old

View File

@@ -55,22 +55,6 @@ flowchart TD
C -->|Three| F[fa:fa-car Car]
\`\`\`
## LaTeX Math
**Inline math:** The Fibonacci sequence is defined as: \$F(n) = F(n-1) + F(n-2)\$ where \$F(0) = 0\$ and \$F(1) = 1\$.
**Block math:**
\$\$F(n) = F(n-1) + F(n-2)\$\$
**Complex equations:**
\$\$
F(n) = \\begin{cases}
0 & \\text{if } n = 0 \\\\
1 & \\text{if } n = 1 \\\\
F(n-1) + F(n-2) & \\text{if } n > 1
\\end{cases}
\$\$
## github classroom links will be replaced by the GithubClassroomAssignmentShareLink setting
[Github Classroom](insert_github_classroom_url)

View File

@@ -12,13 +12,11 @@ import { Spinner } from "@/components/Spinner";
import MeatballIcon from "./MeatballIcon";
import { useSetAssignmentGroupsMutation } from "@/features/canvas/hooks/canvasCourseHooks";
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
import Modal, { useModal } from "@/components/Modal";
export default function AssignmentGroupManagement() {
const { data: settings, isPending } = useLocalCourseSettingsQuery();
const updateSettings = useUpdateLocalCourseSettingsMutation();
const applyInCanvas = useSetAssignmentGroupsMutation(settings.canvasId);
const modal = useModal();
const [assignmentGroups, setAssignmentGroups] = useState<
LocalAssignmentGroup[]
@@ -106,46 +104,17 @@ export default function AssignmentGroupManagement() {
</div>
<br />
<div className="flex justify-end">
<Modal
modalControl={modal}
buttonText="Update Assignment Groups In Canvas"
buttonClass="btn-"
modalWidth="w-1/5"
>
{({ closeModal }) => (
<div>
<div className="text-center font-bold">
DANGER: updating assignment groups can delete assignments and grades from canvas.
</div>
<div className="text-center">
This is only recommended to do at the beginning of a semester. Are you sure you want to continue?
</div>
<br />
<div className="flex justify-around gap-3">
<button
onClick={async () => {
const newSettings = await applyInCanvas.mutateAsync(
settings
);
const newSettings = await applyInCanvas.mutateAsync(settings);
// prevent debounce from resetting
if (newSettings)
setAssignmentGroups(newSettings.assignmentGroups);
if (newSettings) setAssignmentGroups(newSettings.assignmentGroups);
}}
disabled={applyInCanvas.isPending}
className="btn-danger"
>
Yes
Update Assignment Groups In Canvas
</button>
<button onClick={closeModal} disabled={applyInCanvas.isPending}>
No
</button>
</div>
{applyInCanvas.isPending && <Spinner />}
</div>
)}
</Modal>
</div>
{applyInCanvas.isPending && <Spinner />}
{applyInCanvas.isSuccess && (

View File

@@ -1,225 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { canvasQuizService } from "./canvasQuizService";
import { CanvasQuizQuestion } from "@/features/canvas/models/quizzes/canvasQuizQuestionModel";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
// Mock the dependencies
vi.mock("@/services/axiosUtils", () => ({
axiosClient: {
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
},
}));
vi.mock("./canvasServiceUtils", () => ({
canvasApi: "https://test.instructure.com/api/v1",
paginatedRequest: vi.fn(),
}));
vi.mock("./canvasAssignmentService", () => ({
canvasAssignmentService: {
getAll: vi.fn(() => Promise.resolve([])),
delete: vi.fn(() => Promise.resolve()),
},
}));
vi.mock("@/services/htmlMarkdownUtils", () => ({
markdownToHTMLSafe: vi.fn(({ markdownString }) => `<p>${markdownString}</p>`),
}));
vi.mock("@/features/local/utils/timeUtils", () => ({
getDateFromStringOrThrow: vi.fn((dateString) => new Date(dateString)),
}));
vi.mock("@/services/utils/questionHtmlUtils", () => ({
escapeMatchingText: vi.fn((text) => text),
}));
describe("canvasQuizService", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("getQuizQuestions", () => {
it("should fetch and sort quiz questions by position", async () => {
const mockQuestions: CanvasQuizQuestion[] = [
{
id: 3,
quiz_id: 1,
position: 3,
question_name: "Question 3",
question_type: "multiple_choice_question",
question_text: "What is 2+2?",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 1,
quiz_id: 1,
position: 1,
question_name: "Question 1",
question_type: "multiple_choice_question",
question_text: "What is your name?",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 1,
position: 2,
question_name: "Question 2",
question_type: "essay_question",
question_text: "Describe yourself",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
];
const { paginatedRequest } = await import("./canvasServiceUtils");
vi.mocked(paginatedRequest).mockResolvedValue(mockQuestions);
const result = await canvasQuizService.getQuizQuestions(1, 1);
expect(result).toHaveLength(3);
expect(result[0].position).toBe(1);
expect(result[1].position).toBe(2);
expect(result[2].position).toBe(3);
expect(result[0].question_text).toBe("What is your name?");
expect(result[1].question_text).toBe("Describe yourself");
expect(result[2].question_text).toBe("What is 2+2?");
});
it("should handle questions without position", async () => {
const mockQuestions: CanvasQuizQuestion[] = [
{
id: 1,
quiz_id: 1,
question_name: "Question 1",
question_type: "multiple_choice_question",
question_text: "What is your name?",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 1,
question_name: "Question 2",
question_type: "essay_question",
question_text: "Describe yourself",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
];
const { paginatedRequest } = await import("./canvasServiceUtils");
vi.mocked(paginatedRequest).mockResolvedValue(mockQuestions);
const result = await canvasQuizService.getQuizQuestions(1, 1);
expect(result).toHaveLength(2);
// Should maintain original order when no position is specified
});
});
describe("Question order verification (integration test concept)", () => {
it("should detect correct question order", async () => {
// This is a conceptual test showing what the verification should validate
const _localQuiz: LocalQuiz = {
name: "Test Quiz",
description: "A test quiz",
dueAt: "2023-12-01T23:59:00Z",
shuffleAnswers: false,
showCorrectAnswers: true,
oneQuestionAtATime: false,
allowedAttempts: 1,
questions: [
{
text: "What is your name?",
questionType: QuestionType.SHORT_ANSWER,
points: 5,
answers: [],
matchDistractors: [],
},
{
text: "Describe yourself",
questionType: QuestionType.ESSAY,
points: 10,
answers: [],
matchDistractors: [],
},
{
text: "What is 2+2?",
questionType: QuestionType.MULTIPLE_CHOICE,
points: 5,
answers: [
{ text: "3", correct: false },
{ text: "4", correct: true },
{ text: "5", correct: false },
],
matchDistractors: [],
},
],
};
const canvasQuestions: CanvasQuizQuestion[] = [
{
id: 1,
quiz_id: 1,
position: 1,
question_name: "Question 1",
question_type: "short_answer_question",
question_text: "<p>What is your name?</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 1,
position: 2,
question_name: "Question 2",
question_type: "essay_question",
question_text: "<p>Describe yourself</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 3,
quiz_id: 1,
position: 3,
question_name: "Question 3",
question_type: "multiple_choice_question",
question_text: "<p>What is 2+2?</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
];
// Mock the getQuizQuestions to return our test data
const { paginatedRequest } = await import("./canvasServiceUtils");
vi.mocked(paginatedRequest).mockResolvedValue(canvasQuestions);
const result = await canvasQuizService.getQuizQuestions(1, 1);
// Verify the questions are in the expected order
expect(result).toHaveLength(3);
expect(result[0].question_text).toContain("What is your name?");
expect(result[1].question_text).toContain("Describe yourself");
expect(result[2].question_text).toContain("What is 2+2?");
// Verify positions are sequential
expect(result[0].position).toBe(1);
expect(result[1].position).toBe(2);
expect(result[2].position).toBe(3);
});
});
});

View File

@@ -89,68 +89,6 @@ const hackFixQuestionOrdering = async (
await axiosClient.post(url, { order });
};
const verifyQuestionOrder = async (
canvasCourseId: number,
canvasQuizId: number,
localQuiz: LocalQuiz
): Promise<boolean> => {
console.log("Verifying question order in Canvas quiz");
try {
const canvasQuestions = await canvasQuizService.getQuizQuestions(
canvasCourseId,
canvasQuizId
);
// Check if the number of questions matches
if (canvasQuestions.length !== localQuiz.questions.length) {
console.error(
`Question count mismatch: Canvas has ${canvasQuestions.length}, local quiz has ${localQuiz.questions.length}`
);
return false;
}
// Verify that questions are in the correct order by comparing text content
// We'll use a simple approach: strip HTML tags and compare the core text content
const stripHtml = (html: string): string => {
return html.replace(/<[^>]*>/g, '').trim();
};
for (let i = 0; i < localQuiz.questions.length; i++) {
const localQuestion = localQuiz.questions[i];
const canvasQuestion = canvasQuestions[i];
const localQuestionText = localQuestion.text.trim();
const canvasQuestionText = stripHtml(canvasQuestion.question_text).trim();
// Check if the question text content matches (allowing for HTML conversion differences)
if (!canvasQuestionText.includes(localQuestionText) &&
!localQuestionText.includes(canvasQuestionText)) {
console.error(
`Question order mismatch at position ${i}:`,
`Local: "${localQuestionText}"`,
`Canvas: "${canvasQuestionText}"`
);
return false;
}
// Verify position is correct
if (canvasQuestion.position !== undefined && canvasQuestion.position !== i + 1) {
console.error(
`Question position mismatch at index ${i}: Canvas position is ${canvasQuestion.position}, expected ${i + 1}`
);
return false;
}
}
console.log("Question order verification successful");
return true;
} catch (error) {
console.error("Error during question order verification:", error);
return false;
}
};
const hackFixRedundantAssignments = async (canvasCourseId: number) => {
console.log("hack fixing redundant quiz assignments that are auto-created");
const assignments = await canvasAssignmentService.getAll(canvasCourseId);
@@ -199,19 +137,6 @@ const createQuizQuestions = async (
questionAndPositions
);
await hackFixRedundantAssignments(canvasCourseId);
// Verify that the question order in Canvas matches the local quiz order
const orderVerified = await verifyQuestionOrder(
canvasCourseId,
canvasQuizId,
localQuiz
);
if (!orderVerified) {
console.warn(
"Question order verification failed! The quiz was created but the question order may not match the intended order."
);
}
};
export const canvasQuizService = {
@@ -240,21 +165,6 @@ export const canvasQuizService = {
}
},
async getQuizQuestions(
canvasCourseId: number,
canvasQuizId: number
): Promise<CanvasQuizQuestion[]> {
try {
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/questions`;
const questions = await paginatedRequest<CanvasQuizQuestion[]>({ url });
// Sort by position to ensure correct order
return questions.sort((a, b) => (a.position || 0) - (b.position || 0));
} catch (error) {
console.error("Error fetching quiz questions from Canvas:", error);
throw error;
}
},
async create(
canvasCourseId: number,
localQuiz: LocalQuiz,

View File

@@ -1,188 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { LocalQuiz } from "@/features/local/quizzes/models/localQuiz";
import { QuestionType } from "@/features/local/quizzes/models/localQuizQuestion";
import { DayOfWeek } from "@/features/local/course/localCourseSettings";
import { AssignmentSubmissionType } from "@/features/local/assignments/models/assignmentSubmissionType";
// Mock the dependencies
vi.mock("@/services/axiosUtils", () => ({
axiosClient: {
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
},
}));
vi.mock("./canvasServiceUtils", () => ({
canvasApi: "https://test.instructure.com/api/v1",
paginatedRequest: vi.fn(),
}));
vi.mock("./canvasAssignmentService", () => ({
canvasAssignmentService: {
getAll: vi.fn(() => Promise.resolve([])),
delete: vi.fn(() => Promise.resolve()),
},
}));
vi.mock("@/services/htmlMarkdownUtils", () => ({
markdownToHTMLSafe: vi.fn(({ markdownString }) => `<p>${markdownString}</p>`),
}));
vi.mock("@/features/local/utils/timeUtils", () => ({
getDateFromStringOrThrow: vi.fn((dateString) => new Date(dateString)),
}));
vi.mock("@/services/utils/questionHtmlUtils", () => ({
escapeMatchingText: vi.fn((text) => text),
}));
describe("Quiz Order Verification Integration", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("demonstrates the question order verification workflow", async () => {
// This test demonstrates that the verification step is properly integrated
// into the quiz creation workflow
const testQuiz: LocalQuiz = {
name: "Test Quiz - Order Verification",
description: "Testing question order verification",
dueAt: "2023-12-01T23:59:00Z",
shuffleAnswers: false,
showCorrectAnswers: true,
oneQuestionAtATime: false,
allowedAttempts: 1,
questions: [
{
text: "First Question",
questionType: QuestionType.SHORT_ANSWER,
points: 5,
answers: [],
matchDistractors: [],
},
{
text: "Second Question",
questionType: QuestionType.ESSAY,
points: 10,
answers: [],
matchDistractors: [],
},
],
};
// Import the service after mocks are set up
const { canvasQuizService } = await import("./canvasQuizService");
const { axiosClient } = await import("@/services/axiosUtils");
const { paginatedRequest } = await import("./canvasServiceUtils");
// Mock successful quiz creation
vi.mocked(axiosClient.post).mockResolvedValueOnce({
data: { id: 123, title: "Test Quiz - Order Verification" },
});
// Mock question creation responses
vi.mocked(axiosClient.post)
.mockResolvedValueOnce({ data: { id: 1, position: 1 } })
.mockResolvedValueOnce({ data: { id: 2, position: 2 } });
// Mock reordering call
vi.mocked(axiosClient.post).mockResolvedValueOnce({ data: {} });
// Mock assignment cleanup (empty assignments)
vi.mocked(paginatedRequest).mockResolvedValueOnce([]);
// Mock the verification call - questions in correct order
vi.mocked(paginatedRequest).mockResolvedValueOnce([
{
id: 1,
quiz_id: 123,
position: 1,
question_name: "Question 1",
question_type: "short_answer_question",
question_text: "<p>First Question</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 123,
position: 2,
question_name: "Question 2",
question_type: "essay_question",
question_text: "<p>Second Question</p>",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
]);
// Create the quiz and trigger verification
const result = await canvasQuizService.create(12345, testQuiz, {
name: "Test Course",
canvasId: 12345,
assignmentGroups: [],
daysOfWeek: [DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Friday],
startDate: "2023-08-15",
endDate: "2023-12-15",
defaultDueTime: { hour: 23, minute: 59 },
defaultAssignmentSubmissionTypes: [AssignmentSubmissionType.ONLINE_TEXT_ENTRY],
defaultFileUploadTypes: [],
holidays: [],
assets: []
});
// Verify the quiz was created
expect(result).toBe(123);
// Verify that the question verification API call was made
expect(vi.mocked(paginatedRequest)).toHaveBeenCalledWith({
url: "https://test.instructure.com/api/v1/courses/12345/quizzes/123/questions",
});
// The verification would have run and logged success/failure
// In a real scenario, this would catch order mismatches
});
it("demonstrates successful verification workflow", async () => {
const { canvasQuizService } = await import("./canvasQuizService");
const { paginatedRequest } = await import("./canvasServiceUtils");
// Mock questions returned from Canvas in correct order
vi.mocked(paginatedRequest).mockResolvedValueOnce([
{
id: 1,
quiz_id: 1,
position: 1,
question_name: "Question 1",
question_type: "short_answer_question",
question_text: "First question",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
{
id: 2,
quiz_id: 1,
position: 2,
question_name: "Question 2",
question_type: "essay_question",
question_text: "Second question",
correct_comments: "",
incorrect_comments: "",
neutral_comments: "",
},
]);
const result = await canvasQuizService.getQuizQuestions(1, 1);
// Verify questions are returned in correct order
expect(result).toHaveLength(2);
expect(result[0].position).toBe(1);
expect(result[1].position).toBe(2);
expect(result[0].question_text).toBe("First question");
expect(result[1].question_text).toBe("Second question");
});
});