From f87c49a91a17cc2850af003e8127979a2e61e82b Mon Sep 17 00:00:00 2001 From: Torsten Brendgen Date: Mon, 24 Aug 2026 19:38:12 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCge=20Unterst=C3=BCtzung=20f=C3=BCr=20die?= =?UTF-8?q?=20Anzeige=20mehrerer=20aktiver=20Umfragen=20als=20Accordion=20?= =?UTF-8?q?oder=20Slider=20hinzu=20und=20aktualisiere=20die=20Version=20au?= =?UTF-8?q?f=201.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- config/package-solution.json | 2 +- package.json | 2 +- .../DynamicPollWebPart.manifest.json | 1 + .../dynamicPoll/DynamicPollWebPart.ts | 43 +++++---- src/webparts/dynamicPoll/PollCore.ts | 6 +- src/webparts/dynamicPoll/PollDataService.ts | 40 +++++---- src/webparts/dynamicPoll/PollTypes.ts | 2 +- .../components/DynamicPoll.module.scss | 31 ++++++- .../components/DynamicPollCollection.tsx | 88 +++++++++++++++++++ .../components/IDynamicPollProps.ts | 2 +- src/webparts/dynamicPoll/loc/de-de.js | 9 +- src/webparts/dynamicPoll/loc/en-us.js | 9 +- src/webparts/dynamicPoll/loc/myStrings.d.ts | 3 +- tests/PollCore.test.ts | 16 +++- tests/validate-project.js | 6 +- 16 files changed, 214 insertions(+), 51 deletions(-) create mode 100644 src/webparts/dynamicPoll/components/DynamicPollCollection.tsx diff --git a/README.md b/README.md index 0e1ff39..4beda02 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Ein eigenstaendiges, mit SharePoint Server Subscription Edition kompatibles SPFx ## Funktionen -- aktive Umfrage anhand von Aktiv-Flag sowie optionalem Start- und Enddatum +- alle aktiven Umfragen anhand von Aktiv-Flag sowie optionalem Start- und Enddatum +- umschaltbare Darstellung mehrerer aktiver Umfragen als Accordion oder Slider mit Punktenavigation - genau eine Antwort je angemeldetem Benutzer und Umfrage - unmittelbare Ergebnisanzeige mit Stimmen und Prozentwerten - optional sichtbare Ergebnisse vor der eigenen Abstimmung @@ -44,7 +45,7 @@ Nein Anderer Termin ``` -Leerzeilen und doppelte Eintraege werden ignoriert. `IsActive` aktivieren; Start- und Enddatum koennen leer bleiben. Sind mehrere Umfragen gleichzeitig aktiv, wird die mit dem neuesten Startdatum verwendet. +Leerzeilen und doppelte Eintraege werden ignoriert. `IsActive` aktivieren; Start- und Enddatum koennen leer bleiben. Sind mehrere Umfragen gleichzeitig aktiv, werden sie nach Startdatum sortiert und entsprechend dem im Property Pane gewählten Accordion- oder Slider-Modus angezeigt. Danach das WebPart auf einer modernen Seite einfuegen und im Property Pane die Listen `Polls` und `Poll Answers` auswaehlen. diff --git a/config/package-solution.json b/config/package-solution.json index bfa584a..3d7f4cb 100644 --- a/config/package-solution.json +++ b/config/package-solution.json @@ -4,7 +4,7 @@ "name": "dynamic-poll-web-part", "title": "dynamic-poll-web-part", "id": "900239c9-b6c7-495d-a386-2d08a81a66f3", - "version": "1.0.0.0", + "version": "1.1.0.0", "includeClientSideAssets": true, "skipFeatureDeployment": true }, diff --git a/package.json b/package.json index 7c84828..4272a17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dynamic-poll-web-part", - "version": "1.0.0", + "version": "1.1.0", "private": true, "main": "lib/index.js", "engines": { "node": ">=6.9.0 <9.0.0" }, diff --git a/src/webparts/dynamicPoll/DynamicPollWebPart.manifest.json b/src/webparts/dynamicPoll/DynamicPollWebPart.manifest.json index 516c137..c2ca047 100644 --- a/src/webparts/dynamicPoll/DynamicPollWebPart.manifest.json +++ b/src/webparts/dynamicPoll/DynamicPollWebPart.manifest.json @@ -19,6 +19,7 @@ "pollListId": "", "answerListId": "", "showResultsBeforeVote": false, + "displayMode": "slider", "refreshSeconds": 60 } }] diff --git a/src/webparts/dynamicPoll/DynamicPollWebPart.ts b/src/webparts/dynamicPoll/DynamicPollWebPart.ts index f8a06ed..719be3f 100644 --- a/src/webparts/dynamicPoll/DynamicPollWebPart.ts +++ b/src/webparts/dynamicPoll/DynamicPollWebPart.ts @@ -6,17 +6,17 @@ import { PropertyPaneDropdown, PropertyPaneSlider, PropertyPaneToggle } from '@microsoft/sp-webpart-base'; import * as strings from 'DynamicPollWebPartStrings'; -import DynamicPoll from './components/DynamicPoll'; -import { IDynamicPollProps } from './components/IDynamicPollProps'; +import DynamicPollCollection, { IDynamicPollCollectionProps } from './components/DynamicPollCollection'; import styles from './components/DynamicPoll.module.scss'; import { PollDataService } from './PollDataService'; -import { IListOption, IPollSnapshot } from './PollTypes'; +import { IListOption, IPollSnapshot, PollDisplayMode } from './PollTypes'; export interface IDynamicPollWebPartProps { pollListId: string; answerListId: string; showResultsBeforeVote: boolean; refreshSeconds: number; + displayMode: PollDisplayMode; } export default class DynamicPollWebPart extends BaseClientSideWebPart { @@ -68,6 +68,13 @@ export default class DynamicPollWebPart extends BaseClientSideWebPart { + return this.service.getSnapshots(this.properties.pollListId, this.properties.answerListId, this.userKey) + .then((snapshots: IPollSnapshot[]): void => { if (this.disposed || version !== this.requestVersion) { return; } - if (!snapshot) { this.renderStatus(strings.NoPollMessage, false); return; } - const element: React.ReactElement = React.createElement(DynamicPoll, { - snapshot: snapshot, + if (!snapshots.length) { this.renderStatus(strings.NoPollMessage, false); return; } + const pollStrings = { + voteButton: strings.VoteButton, votingButton: strings.VotingButton, + thanksMessage: strings.ThanksMessage, resultsHeading: strings.ResultsHeading, + votesLabel: strings.VotesLabel, oneVoteLabel: strings.OneVoteLabel, + selectAnswerMessage: strings.SelectAnswerMessage, voteErrorMessage: strings.VoteErrorMessage, + alreadyVotedMessage: strings.AlreadyVotedMessage, + previousPollLabel: strings.PreviousPollLabel, nextPollLabel: strings.NextPollLabel, + pollNavigationLabel: strings.PollNavigationLabel + }; + const element: React.ReactElement = React.createElement(DynamicPollCollection, { + snapshots: snapshots, + displayMode: this.properties.displayMode === 'accordion' ? 'accordion' : 'slider', showResultsBeforeVote: this.properties.showResultsBeforeVote === true, - strings: { - voteButton: strings.VoteButton, votingButton: strings.VotingButton, - thanksMessage: strings.ThanksMessage, resultsHeading: strings.ResultsHeading, - votesLabel: strings.VotesLabel, oneVoteLabel: strings.OneVoteLabel, - selectAnswerMessage: strings.SelectAnswerMessage, voteErrorMessage: strings.VoteErrorMessage, - alreadyVotedMessage: strings.AlreadyVotedMessage - }, - onVote: (answer: string): Promise => this.vote(snapshot.poll.id, answer) + strings: pollStrings, + onVote: (pollId: number, answer: string): Promise => this.vote(pollId, answer) }); ReactDom.render(element, this.domElement); }).catch((error: Error): void => { diff --git a/src/webparts/dynamicPoll/PollCore.ts b/src/webparts/dynamicPoll/PollCore.ts index de005e4..c62cd4e 100644 --- a/src/webparts/dynamicPoll/PollCore.ts +++ b/src/webparts/dynamicPoll/PollCore.ts @@ -15,6 +15,10 @@ export function parsePollOptionLines(values: string[]): string[] { } export function selectActivePoll(polls: IPoll[], now: Date): IPoll | undefined { + return selectActivePolls(polls, now)[0]; +} + +export function selectActivePolls(polls: IPoll[], now: Date): IPoll[] { const timestamp: number = now.getTime(); const active: IPoll[] = polls.filter((poll: IPoll): boolean => poll.isActive && poll.options.length > 0 && @@ -22,7 +26,7 @@ export function selectActivePoll(polls: IPoll[], now: Date): IPoll | undefined { (!poll.endDate || poll.endDate.getTime() >= timestamp)); active.sort((left: IPoll, right: IPoll): number => dateValue(right.startDate) - dateValue(left.startDate) || right.id - left.id); - return active[0]; + return active; } export function calculatePollResults(options: string[], answers: IPollAnswer[]): IPollResult[] { diff --git a/src/webparts/dynamicPoll/PollDataService.ts b/src/webparts/dynamicPoll/PollDataService.ts index 4d69ee3..14e1401 100644 --- a/src/webparts/dynamicPoll/PollDataService.ts +++ b/src/webparts/dynamicPoll/PollDataService.ts @@ -1,6 +1,6 @@ import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; import { WebPartContext } from '@microsoft/sp-webpart-base'; -import { calculatePollResults, findUserAnswer, parsePollOptionLines, selectActivePoll } from './PollCore'; +import { calculatePollResults, findUserAnswer, parsePollOptionLines, selectActivePolls } from './PollCore'; import { IListOption, IPoll, IPollAnswer, IPollSnapshot } from './PollTypes'; export class PollDataService { @@ -15,24 +15,26 @@ export class PollDataService { }))); } - public getSnapshot(pollListId: string, answerListId: string, userKey: string): Promise { - return this.getActivePoll(pollListId).then((poll: IPoll): Promise => { - if (!poll) { return Promise.resolve(undefined); } - return this.getAnswers(answerListId, poll.id).then((answers: IPollAnswer[]): IPollSnapshot => { - const results = calculatePollResults(poll.options, answers); - return { - poll: poll, - results: results, - totalVotes: results.reduce((total, result): number => total + result.votes, 0), - userAnswer: findUserAnswer(answers, userKey) - }; - }); + public getSnapshots(pollListId: string, answerListId: string, userKey: string): Promise { + return this.getActivePolls(pollListId).then((polls: IPoll[]): Promise => { + const requests: Array> = polls.map((poll: IPoll): Promise => + this.getAnswers(answerListId, poll.id).then((answers: IPollAnswer[]): IPollSnapshot => { + const results = calculatePollResults(poll.options, answers); + return { + poll: poll, + results: results, + totalVotes: results.reduce((total, result): number => total + result.votes, 0), + userAnswer: findUserAnswer(answers, userKey) + }; + })); + return Promise.all(requests); }); } public vote(pollListId: string, answerListId: string, pollId: number, answer: string, userKey: string): Promise { - return this.getActivePoll(pollListId).then((poll: IPoll): Promise => { - if (!poll || poll.id !== pollId || poll.options.indexOf(answer) < 0) { + return this.getActivePolls(pollListId).then((polls: IPoll[]): Promise => { + const poll: IPoll = polls.filter((candidate: IPoll): boolean => candidate.id === pollId)[0]; + if (!poll || poll.options.indexOf(answer) < 0) { return Promise.reject(new Error('POLL_NOT_ACTIVE')); } return this.saveVote(answerListId, pollId, answer, userKey); @@ -57,12 +59,12 @@ export class PollDataService { }); } - private getActivePoll(listId: string): Promise { - return this.getOptionsFieldName(listId).then((optionsField: string): Promise => { + private getActivePolls(listId: string): Promise { + return this.getOptionsFieldName(listId).then((optionsField: string): Promise => { const url: string = this.webUrl + "/_api/web/lists(guid'" + this.validateGuid(listId) + "')/items" + '?$select=Id,Title,Question,' + optionsField + ',StartDate,EndDate,IsActive&$filter=IsActive eq 1&$orderby=StartDate desc&$top=100'; - return this.getJson(url).then((payload: any): IPoll | undefined => { // tslint:disable-line:no-any + return this.getJson(url).then((payload: any): IPoll[] => { // tslint:disable-line:no-any const polls: IPoll[] = this.results(payload).map((item: any): IPoll => ({ // tslint:disable-line:no-any id: Number(item.Id), title: String(item.Title || ''), @@ -72,7 +74,7 @@ export class PollDataService { endDate: this.toDate(item.EndDate), isActive: item.IsActive === true || item.IsActive === 1 })); - return selectActivePoll(polls, new Date()); + return selectActivePolls(polls, new Date()); }); }); } diff --git a/src/webparts/dynamicPoll/PollTypes.ts b/src/webparts/dynamicPoll/PollTypes.ts index 6f3a43f..96718d9 100644 --- a/src/webparts/dynamicPoll/PollTypes.ts +++ b/src/webparts/dynamicPoll/PollTypes.ts @@ -16,4 +16,4 @@ export interface IPollSnapshot { totalVotes: number; userAnswer: string; } - +export type PollDisplayMode = 'accordion' | 'slider'; diff --git a/src/webparts/dynamicPoll/components/DynamicPoll.module.scss b/src/webparts/dynamicPoll/components/DynamicPoll.module.scss index db0abbf..d375434 100644 --- a/src/webparts/dynamicPoll/components/DynamicPoll.module.scss +++ b/src/webparts/dynamicPoll/components/DynamicPoll.module.scss @@ -2,7 +2,6 @@ .poll { background: "[theme: white, default: #ffffff]"; - border: 1px solid #edebe9; box-sizing: border-box; color: "[theme: neutralPrimary, default: #323130]"; font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; @@ -67,4 +66,32 @@ white-space: nowrap; width: 1px; } - +.collection { width: 100%; } +.accordionItem { border-bottom: 1px solid "[theme: neutralLight, default: #edebe9]"; } +.accordionHeader { + align-items: center; + background: "[theme: white, default: #ffffff]"; + border: 0; + color: "[theme: neutralPrimary, default: #323130]"; + cursor: pointer; + display: flex; + font: inherit; + font-weight: 600; + justify-content: space-between; + min-height: 44px; + padding: 8px 12px; + text-align: left; + width: 100%; +} +.accordionHeader:hover { background: "[theme: neutralLighter, default: #f3f2f1]"; } +.accordionHeader:focus { outline: 2px solid "[theme: themePrimary, default: #0078d4]"; outline-offset: -2px; } +.accordionChevron { font-family: 'FabricMDL2Icons'; font-size: 12px; margin-left: 12px; } +.accordionPanel .poll { padding-top: 14px; } +.sliderNavigation { align-items: center; display: flex; justify-content: center; margin-top: 8px; } +.sliderArrow { background: transparent; border: 0; color: "[theme: themePrimary, default: #0078d4]"; cursor: pointer; font-size: 28px; height: 32px; line-height: 24px; padding: 0; width: 36px; } +.sliderArrow:hover { background: "[theme: neutralLighter, default: #f3f2f1]"; } +.sliderArrow:focus { outline: 2px solid "[theme: themePrimary, default: #0078d4]"; } +.pagination { align-items: center; display: flex; gap: 8px; padding: 0 8px; } +.paginationDot, .paginationDotActive { background: "[theme: neutralTertiary, default: #a19f9d]"; border: 0; border-radius: 50%; cursor: pointer; height: 8px; padding: 0; width: 8px; } +.paginationDotActive { background: "[theme: themePrimary, default: #0078d4]"; height: 10px; width: 10px; } +.paginationDot:focus, .paginationDotActive:focus { outline: 2px solid "[theme: themePrimary, default: #0078d4]"; outline-offset: 2px; } diff --git a/src/webparts/dynamicPoll/components/DynamicPollCollection.tsx b/src/webparts/dynamicPoll/components/DynamicPollCollection.tsx new file mode 100644 index 0000000..cad846a --- /dev/null +++ b/src/webparts/dynamicPoll/components/DynamicPollCollection.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { IPollSnapshot, PollDisplayMode } from '../PollTypes'; +import DynamicPoll from './DynamicPoll'; +import { IDynamicPollStrings } from './IDynamicPollProps'; +import styles from './DynamicPoll.module.scss'; + +export interface IDynamicPollCollectionProps { + displayMode: PollDisplayMode; + snapshots: IPollSnapshot[]; + showResultsBeforeVote: boolean; + strings: IDynamicPollStrings; + onVote: (pollId: number, answer: string) => Promise; +} + +export interface IDynamicPollCollectionState { + activeIndex: number; +} + +export default class DynamicPollCollection + extends React.Component { + + public constructor(props: IDynamicPollCollectionProps) { + super(props); + this.state = { activeIndex: 0 }; + } + + public componentWillReceiveProps(nextProps: IDynamicPollCollectionProps): void { + if (this.state.activeIndex >= nextProps.snapshots.length) { this.setState({ activeIndex: 0 }); } + } + + public render(): JSX.Element { + return this.props.displayMode === 'accordion' ? this.renderAccordion() : this.renderSlider(); + } + + private renderAccordion(): JSX.Element { + return
+ {this.props.snapshots.map((snapshot: IPollSnapshot, index: number): JSX.Element => { + const expanded: boolean = index === this.state.activeIndex; + const panelId: string = 'poll-panel-' + snapshot.poll.id; + return
+ + {expanded &&
+ {this.renderPoll(snapshot)} +
} +
; + })} +
; + } + + private renderSlider(): JSX.Element { + const count: number = this.props.snapshots.length; + const active: number = Math.min(this.state.activeIndex, Math.max(count - 1, 0)); + const snapshot: IPollSnapshot = this.props.snapshots[active]; + return
+ {snapshot && this.renderPoll(snapshot)} + {count > 1 &&
+ +
+ {this.props.snapshots.map((item: IPollSnapshot, index: number): JSX.Element => +
+ +
} +
; + } + + private renderPoll(snapshot: IPollSnapshot): JSX.Element { + return => this.props.onVote(snapshot.poll.id, answer)} />; + } + + private move(offset: number): void { + const count: number = this.props.snapshots.length; + if (count) { this.setState({ activeIndex: (this.state.activeIndex + offset + count) % count }); } + } +} diff --git a/src/webparts/dynamicPoll/components/IDynamicPollProps.ts b/src/webparts/dynamicPoll/components/IDynamicPollProps.ts index 47b5af4..e5c9fd7 100644 --- a/src/webparts/dynamicPoll/components/IDynamicPollProps.ts +++ b/src/webparts/dynamicPoll/components/IDynamicPollProps.ts @@ -4,6 +4,7 @@ export interface IDynamicPollStrings { voteButton: string; votingButton: string; thanksMessage: string; resultsHeading: string; votesLabel: string; oneVoteLabel: string; selectAnswerMessage: string; voteErrorMessage: string; alreadyVotedMessage: string; + previousPollLabel: string; nextPollLabel: string; pollNavigationLabel: string; } export interface IDynamicPollProps { snapshot: IPollSnapshot; @@ -11,4 +12,3 @@ export interface IDynamicPollProps { strings: IDynamicPollStrings; onVote: (answer: string) => Promise; } - diff --git a/src/webparts/dynamicPoll/loc/de-de.js b/src/webparts/dynamicPoll/loc/de-de.js index 575d32c..0d9d107 100644 --- a/src/webparts/dynamicPoll/loc/de-de.js +++ b/src/webparts/dynamicPoll/loc/de-de.js @@ -5,6 +5,9 @@ define([], function() { return { "PollListLabel": "Umfrageliste", "AnswerListLabel": "Antwortliste", "ShowResultsBeforeVoteLabel": "Ergebnisse vor der Abstimmung anzeigen", + "DisplayModeLabel": "Mehrere aktive Umfragen anzeigen als", + "AccordionModeLabel": "Accordion", + "SliderModeLabel": "Slider mit Seitennavigation", "RefreshSecondsLabel": "Automatische Aktualisierung (Sekunden)", "SelectOption": "Auswählen...", "ConfigureMessage": "Wählen Sie in den WebPart-Einstellungen die Umfrage- und Antwortliste aus.", @@ -19,6 +22,8 @@ define([], function() { return { "ResultsHeading": "Ergebnis", "VotesLabel": "Stimmen", "OneVoteLabel": "Stimme", - "SelectAnswerMessage": "Wählen Sie vor der Abstimmung eine Antwort aus." + "SelectAnswerMessage": "Wählen Sie vor der Abstimmung eine Antwort aus.", + "PreviousPollLabel": "Vorherige Umfrage", + "NextPollLabel": "Nächste Umfrage", + "PollNavigationLabel": "Zwischen aktiven Umfragen wechseln" }; }); - diff --git a/src/webparts/dynamicPoll/loc/en-us.js b/src/webparts/dynamicPoll/loc/en-us.js index 14d14df..d6c2f52 100644 --- a/src/webparts/dynamicPoll/loc/en-us.js +++ b/src/webparts/dynamicPoll/loc/en-us.js @@ -5,6 +5,9 @@ define([], function() { return { "PollListLabel": "Poll list", "AnswerListLabel": "Answer list", "ShowResultsBeforeVoteLabel": "Show results before voting", + "DisplayModeLabel": "Show multiple active polls as", + "AccordionModeLabel": "Accordion", + "SliderModeLabel": "Slider with pagination", "RefreshSecondsLabel": "Automatic refresh (seconds)", "SelectOption": "Select...", "ConfigureMessage": "Select the poll and answer lists in the web part settings.", @@ -19,6 +22,8 @@ define([], function() { return { "ResultsHeading": "Results", "VotesLabel": "votes", "OneVoteLabel": "vote", - "SelectAnswerMessage": "Select an answer before voting." + "SelectAnswerMessage": "Select an answer before voting.", + "PreviousPollLabel": "Previous poll", + "NextPollLabel": "Next poll", + "PollNavigationLabel": "Switch between active polls" }; }); - diff --git a/src/webparts/dynamicPoll/loc/myStrings.d.ts b/src/webparts/dynamicPoll/loc/myStrings.d.ts index 1f30e8e..ac9dafd 100644 --- a/src/webparts/dynamicPoll/loc/myStrings.d.ts +++ b/src/webparts/dynamicPoll/loc/myStrings.d.ts @@ -5,9 +5,10 @@ declare interface IDynamicPollWebPartStrings { LoadingMessage: string; NoPollMessage: string; LoadErrorMessage: string; VoteErrorMessage: string; AlreadyVotedMessage: string; VoteButton: string; VotingButton: string; ThanksMessage: string; ResultsHeading: string; VotesLabel: string; OneVoteLabel: string; SelectAnswerMessage: string; + DisplayModeLabel: string; AccordionModeLabel: string; SliderModeLabel: string; + PreviousPollLabel: string; NextPollLabel: string; PollNavigationLabel: string; } declare module 'DynamicPollWebPartStrings' { const strings: IDynamicPollWebPartStrings; export = strings; } - diff --git a/tests/PollCore.test.ts b/tests/PollCore.test.ts index 930c350..b54c42a 100644 --- a/tests/PollCore.test.ts +++ b/tests/PollCore.test.ts @@ -1,4 +1,10 @@ -import { calculatePollResults, findUserAnswer, parsePollOptionLines, selectActivePoll } from '../src/webparts/dynamicPoll/PollCore'; +import { + calculatePollResults, + findUserAnswer, + parsePollOptionLines, + selectActivePoll, + selectActivePolls +} from '../src/webparts/dynamicPoll/PollCore'; import { IPoll, IPollAnswer } from '../src/webparts/dynamicPoll/PollTypes'; let passed: number = 0; @@ -18,6 +24,14 @@ const selected: IPoll = selectActivePoll([ startDate: new Date('2026-08-24T00:00:00Z') }) ], now); assert('current poll is selected by activation window', !!selected && selected.id === 2); +const activePolls: IPoll[] = selectActivePolls([ + poll({ id: 2, question: 'newer', options: ['A'], isActive: true, + startDate: new Date('2026-08-23T00:00:00Z') }), + poll({ id: 4, question: 'older', options: ['A'], isActive: true, + startDate: new Date('2026-08-21T00:00:00Z') }) +], now); +assert('all active polls are returned in activation order', + activePolls.length === 2 && activePolls[0].id === 2 && activePolls[1].id === 4); const answers: IPollAnswer[] = [ { id: 1, userKey: 'one@contoso.local', answer: 'A' }, diff --git a/tests/validate-project.js b/tests/validate-project.js index c31d640..6248d53 100644 --- a/tests/validate-project.js +++ b/tests/validate-project.js @@ -15,7 +15,7 @@ var service = read('src/webparts/dynamicPoll/PollDataService.ts'); var component = read('src/webparts/dynamicPoll/components/DynamicPoll.tsx'); var styles = read('src/webparts/dynamicPoll/components/DynamicPoll.module.scss'); -assert(packageJson.version === '1.0.0' && solution.version === '1.0.0.0', 'Version mismatch.'); +assert(packageJson.version === '1.1.0' && solution.version === '1.1.0.0', 'Version mismatch.'); assert(solution.includeClientSideAssets === true && solution.skipFeatureDeployment === true, 'Central deployment missing.'); assert(packageJson.name === 'dynamic-poll-web-part' && solution.name === 'dynamic-poll-web-part' && solution.title === 'dynamic-poll-web-part', 'Dynamic Poll package naming is inconsistent.'); @@ -24,6 +24,10 @@ assert(json('config/package-solution.json').paths.zippedPackage === 'solution/dy assert(manifest.componentType === 'WebPart', 'Component is not a web part.'); assert(webPart.indexOf("PropertyPaneDropdown('pollListId'") >= 0 && webPart.indexOf("PropertyPaneDropdown('answerListId'") >= 0, 'List selection is missing.'); +assert(webPart.indexOf("PropertyPaneDropdown('displayMode'") >= 0 && + webPart.indexOf('DynamicPollCollection') >= 0, 'Multiple-poll display mode is missing.'); +assert(service.indexOf('getSnapshots') >= 0 && service.indexOf('getActivePolls') >= 0, + 'All active polls must be loaded and remain voteable.'); assert(service.indexOf("'PollId': pollId") >= 0 && service.indexOf("'Answer': answer") >= 0, 'Vote persistence is missing.'); assert(service.indexOf("new Error('ALREADY_VOTED')") >= 0, 'Duplicate vote protection is missing.'); assert(service.indexOf("new Error('POLL_NOT_ACTIVE')") >= 0, 'Active poll and answer validation is missing.');