mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-10-30 18:58:39 -04:00
* Add crop_job_step association to Job model and migration - Introduced a has_many association in CropJobStep for jobs with dependent nullify. - Added crop_job_step reference to Job model with optional association. - Created migration to add crop_job_step reference to jobs table with foreign key. - Updated schema to reflect the new association. - Added tests for crop_job_step association in Job model. * Add ApplyCropJobTemplate service and for_crop scope to CropJobTemplate model * Add job idea dismissal feature and related services - Introduced JobIdeaDismissal model to manage dismissed job ideas for members. - Added associations and validations for job idea dismissals in the Member model. - Created JobIdeas service to generate job ideas based on templates and last year's activities. - Implemented JobIdeas::LastYear and JobIdeas::Templates classes to fetch relevant job ideas. - Added migration for job_idea_dismissals table and updated schema. - Developed tests for job idea dismissal functionality and related services. * Add PlantedLastYear service and update JobIdeas to include it * Add action attribute to Job model and update related files * Refactor JobIdeas service: replace LastYear and PlantedLastYear with PastJobs and PastPlantings; update related specs * Add community and nearby job ideas sources; implement shown and more methods in JobIdeas service * Implement job idea dismissal duration and snooze functionality; add hidden_until attribute and related specs * Add Harvest class to JobIdeas service for managing harvest-related job ideas; implement ideas generation and related specs * Add JobIdeas feature: implement JobIdeasController, views, and related services for job suggestions; include functionality for adding, hiding, and snoozing job ideas * Refactor button options in job ideas view for improved readability and maintainability * Enhance Job Ideas feature: add styling for idea cards, implement filtering options, and update controller logic to manage skipped sources; improve localization for job idea reasons and sections * Add job ideas count and badge display; implement helper methods and update views for better visibility of job ideas * Enhance Job Ideas feature: add UI elements for job suggestions, implement schedule display for template jobs, and improve localization; update related specs for new functionality * Enhance Job Ideas feature: implement overlay for job suggestions, add animations for job transitions, and improve UI components; update controller and helper methods for better handling of job ideas * Refactor Job Ideas feature: remove badge display and related tests; update UI components for better layout and visibility of job suggestions * Refactor Jobs view and update specs: improve layout for job groups and enhance test coverage for jobs index with ideas * Refactor Job Ideas feature: replace dismiss! with hide! method, update job count logic, and enhance UI components for better visibility and layout * Add job ideas feature: implement ideas banner on homepage, enhance overlay logic, and add tests for job ideas display * widen the range for suggseting jobs * Fix comment to accurately describe planted crops timeframe in PastPlantings class
193 lines
7.0 KiB
JavaScript
193 lines
7.0 KiB
JavaScript
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {addedBlock} from './job_ideas';
|
|
|
|
// The ideas overlay: opened from the links, choosing an idea without leaving the
|
|
// page, and showing the jobs an idea made.
|
|
|
|
const idea = (key, name) => `
|
|
<div class="idea" data-key="${key}">
|
|
<div class="idea-title">${name}</div>
|
|
<form class="idea-form button_to" action="/job_ideas" method="post">
|
|
<input type="hidden" name="key" value="${key}"><button type="submit">Add</button>
|
|
</form>
|
|
</div>`;
|
|
|
|
const page = () => `
|
|
<meta name="csrf-token" content="token-123">
|
|
<a href="/job_ideas" data-job-ideas-open>Ideas</a>
|
|
<a class="notepad-ideas" href="/job_ideas"><span class="notepad-ideas-text">We have 2</span></a>
|
|
<div id="notepad-groups"><div class="notepad-job" data-id="1">Water</div></div>
|
|
<div class="modal" id="job-ideas-modal" data-url="/job_ideas"><div id="job-ideas-body"></div></div>`;
|
|
|
|
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
const json = (data) => ({ok: true, json: () => Promise.resolve(data)});
|
|
const html = (text) => ({ok: true, text: () => Promise.resolve(text)});
|
|
const body = () => document.getElementById('job-ideas-body');
|
|
const submit = (form) => form.dispatchEvent(new Event('submit', {bubbles: true, cancelable: true}));
|
|
|
|
// The script wires itself up when the page has loaded. Import it fresh for each
|
|
// test and run that step by hand.
|
|
const start = async () => {
|
|
vi.resetModules();
|
|
const listen = vi.spyOn(document, 'addEventListener').mockImplementation(() => {});
|
|
await import('./job_ideas');
|
|
const handlers = listen.mock.calls.filter(([name]) => name === 'DOMContentLoaded').map(([, handler]) => handler);
|
|
listen.mockRestore();
|
|
handlers.forEach((handler) => handler());
|
|
};
|
|
|
|
let fetchMock;
|
|
let show;
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = page();
|
|
fetchMock = vi.fn();
|
|
show = vi.fn();
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
// Asking for less motion makes every pause end at once.
|
|
vi.stubGlobal('matchMedia', () => ({matches: true}));
|
|
window.bootstrap = {Modal: {getOrCreateInstance: () => ({show})}};
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
delete window.bootstrap;
|
|
});
|
|
|
|
describe('addedBlock', () => {
|
|
const data = {added_label: 'Added 2 jobs', added: [{id: 1, name: 'Stake', due: '5 Nov'}, {id: 2, name: 'Feed', due: null}]};
|
|
|
|
it('says what was added', () => {
|
|
expect(addedBlock(data).querySelector('.idea-added-label').textContent).toBe('✓ Added 2 jobs');
|
|
});
|
|
|
|
it('lists each job with its day', () => {
|
|
expect(Array.from(addedBlock(data).querySelectorAll('li')).map((li) => li.textContent)).toEqual(['5 Nov Stake', 'Feed']);
|
|
});
|
|
|
|
it('brings each job in a moment after the one before', () => {
|
|
expect(Array.from(addedBlock(data).querySelectorAll('li')).map((li) => li.style.animationDelay)).toEqual(['0ms', '90ms']);
|
|
});
|
|
});
|
|
|
|
describe('opening', () => {
|
|
it('loads the ideas into the overlay', async () => {
|
|
fetchMock.mockResolvedValue(html(idea('a', 'Snip')));
|
|
await start();
|
|
document.querySelector('[data-job-ideas-open]').click();
|
|
await flush();
|
|
expect(body().querySelectorAll('.idea').length).toBe(1);
|
|
});
|
|
|
|
it('shows the overlay', async () => {
|
|
fetchMock.mockResolvedValue(html(idea('a', 'Snip')));
|
|
await start();
|
|
document.querySelector('[data-job-ideas-open]').click();
|
|
await flush();
|
|
expect(show).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('arriving with the ideas to show', () => {
|
|
afterEach(() => window.history.replaceState(null, '', '/'));
|
|
|
|
it('opens the overlay', async () => {
|
|
window.history.replaceState(null, '', '/members/brenda/jobs?ideas=1');
|
|
fetchMock.mockResolvedValue(html(idea('a', 'Snip')));
|
|
await start();
|
|
await flush();
|
|
expect(show).toHaveBeenCalled();
|
|
});
|
|
|
|
it('takes the ideas out of the address, so a reload does not open it again', async () => {
|
|
window.history.replaceState(null, '', '/members/brenda/jobs?ideas=1');
|
|
fetchMock.mockResolvedValue(html(idea('a', 'Snip')));
|
|
await start();
|
|
expect(window.location.search).toBe('');
|
|
});
|
|
|
|
it('does not open the overlay without it', async () => {
|
|
fetchMock.mockResolvedValue(html(idea('a', 'Snip')));
|
|
await start();
|
|
await flush();
|
|
expect(show).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('choosing an idea', () => {
|
|
const added = {
|
|
added: [{id: 7, name: 'Plant tomato', due: null}], added_label: 'Added 1 job to your list', count: 1, banner: 'We have 1',
|
|
groups: '<div class="notepad-job" data-id="1">Water</div><div class="notepad-job" data-id="7">Plant tomato</div>',
|
|
list: idea('b', 'Dig'),
|
|
};
|
|
|
|
const choose = async () => {
|
|
fetchMock.mockResolvedValueOnce(html(idea('a', 'Snip') + idea('b', 'Dig'))).mockResolvedValueOnce(json(added));
|
|
await start();
|
|
document.querySelector('[data-job-ideas-open]').click();
|
|
await flush();
|
|
submit(body().querySelector('.idea-form'));
|
|
await flush();
|
|
await flush();
|
|
};
|
|
|
|
it('sends it in the background', async () => {
|
|
await choose();
|
|
expect(new URL(fetchMock.mock.calls[1][0]).pathname).toBe('/job_ideas');
|
|
});
|
|
|
|
it('sends the token', async () => {
|
|
await choose();
|
|
expect(fetchMock.mock.calls[1][1].headers['X-CSRF-Token']).toBe('token-123');
|
|
});
|
|
|
|
it('marks the new job in the notepad', async () => {
|
|
await choose();
|
|
expect(document.querySelector('.notepad-job[data-id="7"]').classList.contains('just-added')).toBe(true);
|
|
});
|
|
|
|
it('does not mark the jobs that were there', async () => {
|
|
await choose();
|
|
expect(document.querySelector('.notepad-job[data-id="1"]').classList.contains('just-added')).toBe(false);
|
|
});
|
|
|
|
it('puts the ideas that are left in the overlay', async () => {
|
|
await choose();
|
|
expect(Array.from(body().querySelectorAll('.idea')).map((el) => el.dataset.key)).toEqual(['b']);
|
|
});
|
|
|
|
it('updates the words in the box', async () => {
|
|
await choose();
|
|
expect(document.querySelector('.notepad-ideas-text').textContent).toBe('We have 1');
|
|
});
|
|
|
|
it('hides the box when there are no ideas left', async () => {
|
|
fetchMock.mockResolvedValueOnce(html(idea('a', 'Snip'))).mockResolvedValueOnce(json({...added, count: 0, list: ''}));
|
|
await start();
|
|
document.querySelector('[data-job-ideas-open]').click();
|
|
await flush();
|
|
submit(body().querySelector('.idea-form'));
|
|
await flush();
|
|
await flush();
|
|
expect(document.querySelector('.notepad-ideas').hidden).toBe(true);
|
|
});
|
|
|
|
it('does not send the choice again once the server has answered', async () => {
|
|
await choose();
|
|
const form = body().querySelector('.idea-form');
|
|
expect(form === null || form.submit === HTMLFormElement.prototype.submit).toBe(true);
|
|
});
|
|
|
|
it('sends the choice the ordinary way if it could not be sent', async () => {
|
|
fetchMock.mockResolvedValueOnce(html(idea('a', 'Snip'))).mockRejectedValueOnce(new Error('offline'));
|
|
await start();
|
|
document.querySelector('[data-job-ideas-open]').click();
|
|
await flush();
|
|
const form = body().querySelector('.idea-form');
|
|
form.submit = vi.fn();
|
|
submit(form);
|
|
await flush();
|
|
expect(form.submit).toHaveBeenCalled();
|
|
});
|
|
});
|