merge: rebase canonical reveal flow onto main
This commit is contained in:
@@ -340,5 +340,26 @@ describe('HostShellComponent gameplay wiring', () => {
|
||||
await component.refreshSession();
|
||||
|
||||
expect(replaceState).toHaveBeenCalledWith(null, '', '#/host/guess/ABCD12');
|
||||
expect(component.canStartRound).toBe(false);
|
||||
expect(component.canShowQuestion).toBe(false);
|
||||
expect(component.canMixAnswers).toBe(false);
|
||||
expect(component.canCalculateScores).toBe(true);
|
||||
});
|
||||
|
||||
it('uses phase_view_model to keep host action surface phase-specific', async () => {
|
||||
const component = new HostShellComponent();
|
||||
|
||||
expect(component.canStartRound).toBe(true);
|
||||
expect(component.canShowQuestion).toBe(false);
|
||||
|
||||
component.session = sessionDetailPayload('lie') as any;
|
||||
expect(component.canStartRound).toBe(false);
|
||||
expect(component.canShowQuestion).toBe(true);
|
||||
expect(component.canMixAnswers).toBe(true);
|
||||
|
||||
component.session = sessionDetailPayload('reveal') as any;
|
||||
expect(component.canRevealScoreboard).toBe(true);
|
||||
expect(component.canStartNextRound).toBe(false);
|
||||
expect(component.canFinishGame).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,18 +21,15 @@ type LeaderboardResponse = FinishGameResponse;
|
||||
|
||||
<div class="panel" [attr.data-client-has-no-audio-output]="clientHasNoAudioOutput">
|
||||
<label>{{ copy('common.session_code') }} <input [(ngModel)]="sessionCode" /></label>
|
||||
<label>{{ copy('host.category') }} <input [(ngModel)]="categorySlug" /></label>
|
||||
<label *ngIf="canStartRound">{{ copy('host.category') }} <input [(ngModel)]="categorySlug" /></label>
|
||||
<button (click)="refreshSession()" [disabled]="loading">{{ copy('common.refresh') }}</button>
|
||||
<button (click)="startRound()" [disabled]="loading">{{ copy('host.start_round') }}</button>
|
||||
<button (click)="showQuestion()" [disabled]="loading || !roundQuestionId">{{ copy('host.show_question') }}</button>
|
||||
<button (click)="mixAnswers()" [disabled]="loading || !roundQuestionId">{{ copy('host.mix_answers') }}</button>
|
||||
<button (click)="calculateScores()" [disabled]="loading || !roundQuestionId">{{ copy('host.calculate_scores') }}</button>
|
||||
<button (click)="loadScoreboard()" [disabled]="loading">{{ copy('host.load_scoreboard') }}</button>
|
||||
<button (click)="startNextRound()" [disabled]="loading">{{ copy('host.start_next_round') }}</button>
|
||||
<button (click)="finishGame()" [disabled]="loading">{{ copy('host.finish_game') }}</button>
|
||||
<button *ngIf="scoreboardError" (click)="loadScoreboard()" [disabled]="loading">{{ copy('host.retry_scoreboard') }}</button>
|
||||
<button *ngIf="nextRoundError" (click)="startNextRound()" [disabled]="loading">{{ copy('host.retry_next_round') }}</button>
|
||||
<button *ngIf="finishError" (click)="finishGame()" [disabled]="loading">{{ copy('host.retry_finish') }}</button>
|
||||
<button *ngIf="canStartRound" (click)="startRound()" [disabled]="loading">{{ copy('host.start_round') }}</button>
|
||||
<button *ngIf="canShowQuestion" (click)="showQuestion()" [disabled]="loading || !roundQuestionId">{{ copy('host.show_question') }}</button>
|
||||
<button *ngIf="canMixAnswers" (click)="mixAnswers()" [disabled]="loading || !roundQuestionId">{{ copy('host.mix_answers') }}</button>
|
||||
<button *ngIf="canCalculateScores" (click)="calculateScores()" [disabled]="loading || !roundQuestionId">{{ copy('host.calculate_scores') }}</button>
|
||||
<button *ngIf="canRevealScoreboard || scoreboardError" (click)="loadScoreboard()" [disabled]="loading">{{ copy(scoreboardError ? 'host.retry_scoreboard' : 'host.load_scoreboard') }}</button>
|
||||
<button *ngIf="canStartNextRound || nextRoundError" (click)="startNextRound()" [disabled]="loading">{{ copy(nextRoundError ? 'host.retry_next_round' : 'host.start_next_round') }}</button>
|
||||
<button *ngIf="canFinishGame || finishError" (click)="finishGame()" [disabled]="loading">{{ copy(finishError ? 'host.retry_finish' : 'host.finish_game') }}</button>
|
||||
</div>
|
||||
|
||||
<p *ngIf="session" class="hint">{{ copy('host.audio_locale_hint') }}: {{ locale }}</p>
|
||||
@@ -132,6 +129,34 @@ export class HostShellComponent implements OnInit, OnDestroy {
|
||||
this.unsubscribeLocale = null;
|
||||
}
|
||||
|
||||
get canStartRound(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.host?.can_start_round ?? !this.session);
|
||||
}
|
||||
|
||||
get canShowQuestion(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.host?.can_show_question);
|
||||
}
|
||||
|
||||
get canMixAnswers(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.host?.can_mix_answers);
|
||||
}
|
||||
|
||||
get canCalculateScores(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.host?.can_calculate_scores);
|
||||
}
|
||||
|
||||
get canRevealScoreboard(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.host?.can_reveal_scoreboard);
|
||||
}
|
||||
|
||||
get canStartNextRound(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.host?.can_start_next_round);
|
||||
}
|
||||
|
||||
get canFinishGame(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.host?.can_finish_game);
|
||||
}
|
||||
|
||||
copy(key: string): string {
|
||||
return t(key, this.locale);
|
||||
}
|
||||
|
||||
@@ -613,4 +613,28 @@ describe('PlayerShellComponent gameplay wiring', () => {
|
||||
expect(component.clientHasNoAudioOutput).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps phone client controls phase-specific and low-complexity', () => {
|
||||
const component = new PlayerShellComponent();
|
||||
|
||||
expect(component.showJoinControls).toBe(true);
|
||||
expect(component.showLieControls).toBe(false);
|
||||
expect(component.showGuessControls).toBe(false);
|
||||
expect(component.showFinalLeaderboard).toBe(false);
|
||||
|
||||
component.session = sessionDetailPayload('lie') as any;
|
||||
component.playerId = 9;
|
||||
component.sessionToken = 'tok';
|
||||
expect(component.showJoinControls).toBe(false);
|
||||
expect(component.showLieControls).toBe(true);
|
||||
expect(component.showGuessControls).toBe(false);
|
||||
|
||||
component.session = sessionDetailPayload('guess', { answers: ['A', 'B'] }) as any;
|
||||
expect(component.showLieControls).toBe(false);
|
||||
expect(component.showGuessControls).toBe(true);
|
||||
|
||||
component.session = sessionDetailPayload('finished', { players: [{ id: 1, nickname: 'Luna', score: 8 }] }) as any;
|
||||
expect(component.showGuessControls).toBe(false);
|
||||
expect(component.showFinalLeaderboard).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -46,9 +46,9 @@ function resolveLocalStorage(): Storage | undefined {
|
||||
|
||||
<div class="panel" [attr.data-client-has-no-audio-output]="clientHasNoAudioOutput">
|
||||
<label>{{ copy('common.session_code') }} <input [(ngModel)]="sessionCode" /></label>
|
||||
<label>{{ copy('player.nickname') }} <input [(ngModel)]="nickname" /></label>
|
||||
<label *ngIf="showJoinControls">{{ copy('player.nickname') }} <input [(ngModel)]="nickname" /></label>
|
||||
<button (click)="refreshSession()" [disabled]="loading">{{ copy('common.refresh') }}</button>
|
||||
<button (click)="joinSession()" [disabled]="loading">{{ copy('player.join') }}</button>
|
||||
<button *ngIf="showJoinControls" (click)="joinSession()" [disabled]="loading">{{ copy('player.join') }}</button>
|
||||
</div>
|
||||
|
||||
<p *ngIf="connectionState === 'reconnecting'" class="error">
|
||||
@@ -68,24 +68,28 @@ function resolveLocalStorage(): Storage | undefined {
|
||||
<p><strong>{{ copy('common.status') }}:</strong> {{ session.session.status }}</p>
|
||||
<p *ngIf="session.round_question"><strong>{{ copy('common.prompt') }}:</strong> {{ session.round_question.prompt }}</p>
|
||||
|
||||
<label>{{ copy('player.lie_label') }} <input [(ngModel)]="lieText" [disabled]="loading || session.session.status !== 'lie'" /></label>
|
||||
<button (click)="submitLie()" [disabled]="loading || session.session.status !== 'lie'">{{ copy('player.submit_lie') }}</button>
|
||||
<button *ngIf="submitError?.kind === 'lie'" (click)="submitLie()" [disabled]="loading">{{ copy('player.retry_lie_submit') }}</button>
|
||||
<ng-container *ngIf="showLieControls">
|
||||
<label>{{ copy('player.lie_label') }} <input [(ngModel)]="lieText" [disabled]="loading" /></label>
|
||||
<button (click)="submitLie()" [disabled]="loading">{{ copy('player.submit_lie') }}</button>
|
||||
<button *ngIf="submitError?.kind === 'lie'" (click)="submitLie()" [disabled]="loading">{{ copy('player.retry_lie_submit') }}</button>
|
||||
</ng-container>
|
||||
|
||||
<div class="answers" *ngIf="session.round_question?.answers?.length">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let answer of session.round_question?.answers"
|
||||
(click)="selectedGuess = answer.text"
|
||||
[class.active]="selectedGuess === answer.text"
|
||||
[disabled]="loading || session.session.status !== 'guess'"
|
||||
>
|
||||
{{ answer.text }}
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="showGuessControls">
|
||||
<div class="answers" *ngIf="session.round_question?.answers?.length">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let answer of session.round_question?.answers"
|
||||
(click)="selectedGuess = answer.text"
|
||||
[class.active]="selectedGuess === answer.text"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ answer.text }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button (click)="submitGuess()" [disabled]="loading || session.session.status !== 'guess' || !selectedGuess">{{ copy('player.submit_guess') }}</button>
|
||||
<button *ngIf="submitError?.kind === 'guess'" (click)="submitGuess()" [disabled]="loading">{{ copy('player.retry_guess_submit') }}</button>
|
||||
<button (click)="submitGuess()" [disabled]="loading || !selectedGuess">{{ copy('player.submit_guess') }}</button>
|
||||
<button *ngIf="submitError?.kind === 'guess'" (click)="submitGuess()" [disabled]="loading">{{ copy('player.retry_guess_submit') }}</button>
|
||||
</ng-container>
|
||||
|
||||
<div class="panel" *ngIf="session.reveal && (session.session.status === 'reveal' || session.session.status === 'scoreboard')">
|
||||
<h3>Reveal</h3>
|
||||
@@ -110,7 +114,7 @@ function resolveLocalStorage(): Storage | undefined {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="session.session.status === 'finished' && finalLeaderboard.length">
|
||||
<div *ngIf="showFinalLeaderboard && finalLeaderboard.length">
|
||||
<h3>{{ copy('player.final_leaderboard') }}</h3>
|
||||
<ol>
|
||||
<li *ngFor="let entry of finalLeaderboard">{{ entry.nickname }}: {{ entry.score }}</li>
|
||||
@@ -320,6 +324,25 @@ export class PlayerShellComponent implements OnInit, OnDestroy {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
get showJoinControls(): boolean {
|
||||
if (!this.session) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(this.session?.phase_view_model?.player?.can_join && !this.playerId && !this.sessionToken);
|
||||
}
|
||||
|
||||
get showLieControls(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.player?.can_submit_lie);
|
||||
}
|
||||
|
||||
get showGuessControls(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.player?.can_submit_guess);
|
||||
}
|
||||
|
||||
get showFinalLeaderboard(): boolean {
|
||||
return Boolean(this.session?.phase_view_model?.player?.can_view_final_result);
|
||||
}
|
||||
|
||||
get loadingMessage(): string {
|
||||
switch (this.loadingTransition) {
|
||||
case 'join':
|
||||
|
||||
Reference in New Issue
Block a user