\:\:map\(\) contains unresolvable type\.$#'
+ identifier: argument.unresolvableType
+ count: 1
+ path: app/Console/Commands/ShowToken.php
+
+ -
+ message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$language\.$#'
+ identifier: property.notFound
+ count: 2
+ path: app/Http/Controllers/LexiconController.php
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..7ec4d2c
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,9 @@
+includes:
+ - ./vendor/larastan/larastan/extension.neon
+ - ./vendor/nesbot/carbon/extension.neon
+ - phpstan-baseline.neon
+
+parameters:
+ level: 5
+ paths:
+ - app/
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php
new file mode 100644
index 0000000..42e1596
--- /dev/null
+++ b/resources/views/home.blade.php
@@ -0,0 +1,58 @@
+@extends('layouts.app')
+
+@section('title', 'OTE v2 Homepage')
+
+@section('content')
+
+
Open Translation Engine v2
+
+
+
Lexicon Statistics
+
+
+
Tokens
+
{{ $stats['tokens'] }}
+
+
+
Languages
+
{{ $stats['languages'] }}
+
+
+
Lexical Entries
+
{{ $stats['lexical_entries'] }}
+
+
+
Attributes
+
{{ $stats['attributes'] }}
+
+
+
Links
+
{{ $stats['links'] }}
+
+
+
+
+
+
Entries per Language
+
+ @foreach ($entriesPerLanguage as $language)
+ - {{ $language->name }}: {{ $language->lexical_entries_count }}
+ @endforeach
+
+
+
+
+
+
+
+@endsection
diff --git a/resources/views/languages/create.blade.php b/resources/views/languages/create.blade.php
index 108ebff..6792a8b 100644
--- a/resources/views/languages/create.blade.php
+++ b/resources/views/languages/create.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Add Language
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Add Language')
+
+@section('content')
Add New Language
-
-
+@endsection
diff --git a/resources/views/languages/edit.blade.php b/resources/views/languages/edit.blade.php
index f36a80e..75ad4b2 100644
--- a/resources/views/languages/edit.blade.php
+++ b/resources/views/languages/edit.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Edit Language
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Edit Language')
+
+@section('content')
Edit Language
-
-
+@endsection
diff --git a/resources/views/languages/index.blade.php b/resources/views/languages/index.blade.php
index a456293..52f21ae 100644
--- a/resources/views/languages/index.blade.php
+++ b/resources/views/languages/index.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Languages
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Languages')
+
+@section('content')
-
-
+@endsection
diff --git a/resources/views/languages/show.blade.php b/resources/views/languages/show.blade.php
new file mode 100644
index 0000000..54b5c07
--- /dev/null
+++ b/resources/views/languages/show.blade.php
@@ -0,0 +1,22 @@
+@extends('layouts.app')
+
+@section('title', 'Language: ' . $language->name)
+
+@section('content')
+
+
Language: {{ $language->name }} ({{ $language->code }})
+
+
Lexical Entries
+
+ @forelse ($language->lexicalEntries as $entry)
+ -
+
+ {{ $entry->token->text }}
+
+
+ @empty
+ - No lexical entries found for this language.
+ @endforelse
+
+
+@endsection
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php
new file mode 100644
index 0000000..e79afb4
--- /dev/null
+++ b/resources/views/layouts/app.blade.php
@@ -0,0 +1,32 @@
+
+
+
+
+
+ @yield('title', 'OTE v2')
+
+
+
+
+
+
+ @yield('content')
+
+
+
diff --git a/resources/views/lexicon/create-attribute.blade.php b/resources/views/lexicon/create-attribute.blade.php
index c81d96c..a9f80d4 100644
--- a/resources/views/lexicon/create-attribute.blade.php
+++ b/resources/views/lexicon/create-attribute.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Add Attribute
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Add Attribute')
+
+@section('content')
Add New Attribute for Entry #{{ $entry->id }}
-
-
+@endsection
diff --git a/resources/views/lexicon/create-link.blade.php b/resources/views/lexicon/create-link.blade.php
index 3608fcb..fed632e 100644
--- a/resources/views/lexicon/create-link.blade.php
+++ b/resources/views/lexicon/create-link.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Add Link
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Add Link')
+
+@section('content')
Add New Link for Entry #{{ $entry->id }}
-
-
+@endsection
diff --git a/resources/views/lexicon/create.blade.php b/resources/views/lexicon/create.blade.php
index 7aba450..8fac0e4 100644
--- a/resources/views/lexicon/create.blade.php
+++ b/resources/views/lexicon/create.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Add Lexical Entry
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Add Lexical Entry')
+
+@section('content')
Add New Lexical Entry
-
-
+@endsection
diff --git a/resources/views/lexicon/edit-attribute.blade.php b/resources/views/lexicon/edit-attribute.blade.php
index f0c27e7..d8442c6 100644
--- a/resources/views/lexicon/edit-attribute.blade.php
+++ b/resources/views/lexicon/edit-attribute.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Edit Attribute
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Edit Attribute')
+
+@section('content')
Edit Attribute for Entry #{{ $entry->id }}
-
-
+@endsection
diff --git a/resources/views/lexicon/edit-link.blade.php b/resources/views/lexicon/edit-link.blade.php
index 2997dfc..0dc30e5 100644
--- a/resources/views/lexicon/edit-link.blade.php
+++ b/resources/views/lexicon/edit-link.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Edit Link
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Edit Link')
+
+@section('content')
Edit Link for Entry #{{ $entry->id }}
-
-
+@endsection
diff --git a/resources/views/lexicon/edit.blade.php b/resources/views/lexicon/edit.blade.php
index d3504db..079c585 100644
--- a/resources/views/lexicon/edit.blade.php
+++ b/resources/views/lexicon/edit.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Edit Lexical Entry
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Edit Lexical Entry')
+
+@section('content')
Edit Lexical Entry
-
-
+@endsection
diff --git a/resources/views/lexicon/index.blade.php b/resources/views/lexicon/index.blade.php
index c2bb005..d3b963a 100644
--- a/resources/views/lexicon/index.blade.php
+++ b/resources/views/lexicon/index.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Lexicon Entries
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Lexicon Entries')
+
+@section('content')
-
-
+@endsection
diff --git a/resources/views/lexicon/show.blade.php b/resources/views/lexicon/show.blade.php
index 41bff11..9d866e6 100644
--- a/resources/views/lexicon/show.blade.php
+++ b/resources/views/lexicon/show.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- {{ $entry->token->text }}
-
-
-
+@extends('layouts.app')
+
+@section('title', $entry->token->text)
+
+@section('content')
← Back to list
{{ $entry->token->text }} ({{ $entry->language->code }})
@@ -31,7 +27,7 @@
@@ -67,7 +63,7 @@
@@ -79,5 +75,4 @@
-
-
+@endsection
diff --git a/resources/views/tokens/create.blade.php b/resources/views/tokens/create.blade.php
index 16b4b61..3db927f 100644
--- a/resources/views/tokens/create.blade.php
+++ b/resources/views/tokens/create.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Add Token
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Add Token')
+
+@section('content')
Add New Token
-
-
+@endsection
diff --git a/resources/views/tokens/edit.blade.php b/resources/views/tokens/edit.blade.php
index 8c41b12..0cc94f2 100644
--- a/resources/views/tokens/edit.blade.php
+++ b/resources/views/tokens/edit.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Edit Token
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Edit Token')
+
+@section('content')
Edit Token
-
-
+@endsection
diff --git a/resources/views/tokens/index.blade.php b/resources/views/tokens/index.blade.php
index d259625..68091a6 100644
--- a/resources/views/tokens/index.blade.php
+++ b/resources/views/tokens/index.blade.php
@@ -1,12 +1,8 @@
-
-
-
-
-
- Tokens
-
-
-
+@extends('layouts.app')
+
+@section('title', 'Tokens')
+
+@section('content')
-
-
+@endsection
diff --git a/resources/views/tokens/show.blade.php b/resources/views/tokens/show.blade.php
new file mode 100644
index 0000000..0004671
--- /dev/null
+++ b/resources/views/tokens/show.blade.php
@@ -0,0 +1,22 @@
+@extends('layouts.app')
+
+@section('title', 'Token: ' . $token->text)
+
+@section('content')
+
+
Token: {{ $token->text }}
+
+
Lexical Entries
+
+
+@endsection
diff --git a/resources/views/validate.blade.php b/resources/views/validate.blade.php
new file mode 100644
index 0000000..0a9d56a
--- /dev/null
+++ b/resources/views/validate.blade.php
@@ -0,0 +1,104 @@
+@extends('layouts.app')
+
+@section('title', 'Data Validation')
+
+@section('content')
+
+
Data Validation Results
+
+ @if (empty($issues))
+
+ All good!
+ No data integrity issues found.
+
+ @else
+
+ Issues found!
+ Please review the data integrity issues below.
+
+
+ @if (isset($issues['duplicate_tokens']))
+
Case-Insensitive Duplicate Tokens
+
+
+
+ | ID |
+ Text |
+
+
+
+ @foreach ($issues['duplicate_tokens'] as $token)
+
+ | {{ $token->id }} |
+ {{ $token->text }} |
+
+ @endforeach
+
+
+ @endif
+
+ @if (isset($issues['duplicate_languages']))
+
Duplicate Language Names
+
+
+
+ | Name |
+
+
+
+ @foreach ($issues['duplicate_languages'] as $language)
+
+ | {{ $language->name }} |
+
+ @endforeach
+
+
+ @endif
+
+ @if (isset($issues['unused_tokens']))
+
Unused Tokens
+
+
+
+ | ID |
+ Text |
+
+
+
+ @foreach ($issues['unused_tokens'] as $token)
+
+ | {{ $token->id }} |
+ {{ $token->text }} |
+
+ @endforeach
+
+
+ @endif
+
+ @if (isset($issues['unused_languages']))
+
Unused Languages
+
+
+
+ | ID |
+ Name |
+
+
+
+ @foreach ($issues['unused_languages'] as $language)
+
+ | {{ $language->id }} |
+ {{ $language->name }} |
+
+ @endforeach
+
+
+ @endif
+
+ @endif
+
+
+
+@endsection
diff --git a/routes/web.php b/routes/web.php
index a536575..a027394 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -1,13 +1,13 @@
route('lexicon.index');
-});
+Route::get('/', [HomeController::class, 'index'])->name('home');
+Route::get('/validate', [HomeController::class, 'validate'])->name('validate');
Route::resources([
'tokens' => TokenController::class,
diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php
new file mode 100644
index 0000000..020699d
--- /dev/null
+++ b/tests/DuskTestCase.php
@@ -0,0 +1,48 @@
+addArguments(collect([
+ $this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
+ '--disable-search-engine-choice-screen',
+ '--disable-smooth-scrolling',
+ ])->unless($this->hasHeadlessDisabled(), function (Collection $items) {
+ return $items->merge([
+ '--disable-gpu',
+ '--headless=new',
+ ]);
+ })->all());
+
+ return RemoteWebDriver::create(
+ $_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? 'http://localhost:9515',
+ DesiredCapabilities::chrome()->setCapability(
+ ChromeOptions::CAPABILITY, $options
+ )
+ );
+ }
+}
diff --git a/tests/Feature/DeleteAttributeCommandTest.php b/tests/Feature/DeleteAttributeCommandTest.php
new file mode 100644
index 0000000..e37a2cc
--- /dev/null
+++ b/tests/Feature/DeleteAttributeCommandTest.php
@@ -0,0 +1,19 @@
+create();
+
+ $this->artisan('ote:delete-attribute', ['id' => $attribute->id])
+ ->expectsOutput("Attribute with ID '{$attribute->id}' has been deleted.")
+ ->assertExitCode(0);
+
+ $this->assertDatabaseMissing('attributes', ['id' => $attribute->id]);
+});
+
+test('the delete attribute command handles non-existent attributes', function () {
+ $this->artisan('ote:delete-attribute', ['id' => 999])
+ ->expectsOutput("Attribute with ID '999' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/DeleteEntryCommandTest.php b/tests/Feature/DeleteEntryCommandTest.php
new file mode 100644
index 0000000..11dba1f
--- /dev/null
+++ b/tests/Feature/DeleteEntryCommandTest.php
@@ -0,0 +1,25 @@
+create();
+ $attribute = Attribute::factory()->create(['lexical_entry_id' => $entry->id]);
+ $link = Link::factory()->create(['source_lexical_entry_id' => $entry->id]);
+
+ $this->artisan('ote:delete-entry', ['id' => $entry->id])
+ ->expectsOutput("Lexical entry with ID '{$entry->id}' has been deleted.")
+ ->assertExitCode(0);
+
+ $this->assertDatabaseMissing('lexical_entries', ['id' => $entry->id]);
+ $this->assertDatabaseMissing('attributes', ['id' => $attribute->id]);
+ $this->assertDatabaseMissing('links', ['id' => $link->id]);
+});
+
+test('the delete entry command handles non-existent entries', function () {
+ $this->artisan('ote:delete-entry', ['id' => 999])
+ ->expectsOutput("Lexical entry with ID '999' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/DeleteLanguageCommandTest.php b/tests/Feature/DeleteLanguageCommandTest.php
new file mode 100644
index 0000000..297a09e
--- /dev/null
+++ b/tests/Feature/DeleteLanguageCommandTest.php
@@ -0,0 +1,22 @@
+create();
+ $language = $entry->language;
+
+ $this->artisan('ote:delete-language', ['language' => $language->code])
+ ->expectsOutput("Language '{$language->code}' and its associated lexical entries have been deleted.")
+ ->assertExitCode(0);
+
+ $this->assertDatabaseMissing('languages', ['id' => $language->id]);
+ $this->assertDatabaseMissing('lexical_entries', ['id' => $entry->id]);
+});
+
+test('the delete language command handles non-existent languages', function () {
+ $this->artisan('ote:delete-language', ['language' => 'non-existent-language'])
+ ->expectsOutput("Language 'non-existent-language' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/DeleteLinkCommandTest.php b/tests/Feature/DeleteLinkCommandTest.php
new file mode 100644
index 0000000..68ad97a
--- /dev/null
+++ b/tests/Feature/DeleteLinkCommandTest.php
@@ -0,0 +1,19 @@
+create();
+
+ $this->artisan('ote:delete-link', ['id' => $link->id])
+ ->expectsOutput("Link with ID '{$link->id}' has been deleted.")
+ ->assertExitCode(0);
+
+ $this->assertDatabaseMissing('links', ['id' => $link->id]);
+});
+
+test('the delete link command handles non-existent links', function () {
+ $this->artisan('ote:delete-link', ['id' => 999])
+ ->expectsOutput("Link with ID '999' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/DeleteTokenCommandTest.php b/tests/Feature/DeleteTokenCommandTest.php
new file mode 100644
index 0000000..c8a33b6
--- /dev/null
+++ b/tests/Feature/DeleteTokenCommandTest.php
@@ -0,0 +1,22 @@
+create();
+ $token = $entry->token;
+
+ $this->artisan('ote:delete-token', ['token' => $token->text])
+ ->expectsOutput("Token '{$token->text}' and its associated lexical entries have been deleted.")
+ ->assertExitCode(0);
+
+ $this->assertDatabaseMissing('tokens', ['id' => $token->id]);
+ $this->assertDatabaseMissing('lexical_entries', ['id' => $entry->id]);
+});
+
+test('the delete token command handles non-existent tokens', function () {
+ $this->artisan('ote:delete-token', ['token' => 'non-existent-token'])
+ ->expectsOutput("Token 'non-existent-token' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php
index 8e57a46..3eece2d 100644
--- a/tests/Feature/ExampleTest.php
+++ b/tests/Feature/ExampleTest.php
@@ -3,10 +3,13 @@
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
+ use RefreshDatabase;
+
/**
* A basic test example.
*/
@@ -14,6 +17,6 @@ public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
- $response->assertStatus(302);
+ $response->assertStatus(200);
}
}
diff --git a/tests/Feature/HomeControllerTest.php b/tests/Feature/HomeControllerTest.php
new file mode 100644
index 0000000..83c4008
--- /dev/null
+++ b/tests/Feature/HomeControllerTest.php
@@ -0,0 +1,57 @@
+count(5)->create();
+ Language::factory()->count(3)->create();
+
+ $response = $this->get('/');
+
+ $response->assertStatus(200);
+ $response->assertSee('Open Translation Engine v2');
+ $response->assertSee('Lexicon Statistics');
+ $response->assertSee('Tokens');
+ $response->assertSee('5');
+ $response->assertSee('Languages');
+ $response->assertSee('3');
+ $response->assertSee('Manage Tokens');
+ $response->assertSee('Manage Languages');
+ $response->assertSee('Manage Lexical Entries');
+ $response->assertSee('Validate Data Integrity');
+ }
+
+ public function test_the_validation_page_loads_correctly_with_no_issues()
+ {
+ $response = $this->get('/validate');
+
+ $response->assertStatus(200);
+ $response->assertSee('Data Validation Results');
+ $response->assertSee('All good!');
+ }
+
+ public function test_the_validation_page_shows_issues()
+ {
+ Token::factory()->create(['text' => 'apple']);
+ Token::factory()->create(['text' => 'Apple']);
+
+ $response = $this->get('/validate');
+
+ $response->assertStatus(200);
+ $response->assertSee('Data Validation Results');
+ $response->assertSee('Issues found!');
+ $response->assertSee('Case-Insensitive Duplicate Tokens');
+ $response->assertSee('apple');
+ $response->assertSee('Apple');
+ }
+}
diff --git a/tests/Feature/LanguageControllerTest.php b/tests/Feature/LanguageControllerTest.php
index a40b2db..d9d16b0 100644
--- a/tests/Feature/LanguageControllerTest.php
+++ b/tests/Feature/LanguageControllerTest.php
@@ -1,6 +1,7 @@
count(3)->create();
@@ -40,3 +41,14 @@
$response->assertRedirect('/languages');
$this->assertDatabaseMissing('languages', ['id' => $language->id]);
});
+
+test('it shows a language and its lexical entries', function () {
+ $entry = LexicalEntry::factory()->create();
+ $language = $entry->language;
+
+ $response = $this->get('/languages/'.$language->id);
+
+ $response->assertStatus(200);
+ $response->assertSee($language->name);
+ $response->assertSee($entry->token->text);
+});
diff --git a/tests/Feature/ListAttributesCommandTest.php b/tests/Feature/ListAttributesCommandTest.php
new file mode 100644
index 0000000..97860e7
--- /dev/null
+++ b/tests/Feature/ListAttributesCommandTest.php
@@ -0,0 +1,18 @@
+create();
+ $attribute2 = Attribute::factory()->create();
+
+ $this->artisan('ote:list-attributes')
+ ->expectsTable(
+ ['ID', 'Lexical Entry ID', 'Key', 'Value'],
+ [
+ [$attribute1->id, $attribute1->lexical_entry_id, $attribute1->key, $attribute1->value],
+ [$attribute2->id, $attribute2->lexical_entry_id, $attribute2->key, $attribute2->value],
+ ]
+ )
+ ->assertExitCode(0);
+});
diff --git a/tests/Feature/ListLanguagesCommandTest.php b/tests/Feature/ListLanguagesCommandTest.php
new file mode 100644
index 0000000..fdda1e6
--- /dev/null
+++ b/tests/Feature/ListLanguagesCommandTest.php
@@ -0,0 +1,18 @@
+create();
+ $language2 = Language::factory()->create();
+
+ $this->artisan('ote:list-languages')
+ ->expectsTable(
+ ['ID', 'Code', 'Name'],
+ [
+ [$language1->id, $language1->code, $language1->name],
+ [$language2->id, $language2->code, $language2->name],
+ ]
+ )
+ ->assertExitCode(0);
+});
diff --git a/tests/Feature/ListLinksCommandTest.php b/tests/Feature/ListLinksCommandTest.php
new file mode 100644
index 0000000..daf8446
--- /dev/null
+++ b/tests/Feature/ListLinksCommandTest.php
@@ -0,0 +1,18 @@
+create();
+ $link2 = Link::factory()->create();
+
+ $this->artisan('ote:list-links')
+ ->expectsTable(
+ ['ID', 'Source Entry ID', 'Target Entry ID', 'Type'],
+ [
+ [$link1->id, $link1->source_lexical_entry_id, $link1->target_lexical_entry_id, $link1->type],
+ [$link2->id, $link2->source_lexical_entry_id, $link2->target_lexical_entry_id, $link2->type],
+ ]
+ )
+ ->assertExitCode(0);
+});
diff --git a/tests/Feature/ListTokensCommandTest.php b/tests/Feature/ListTokensCommandTest.php
new file mode 100644
index 0000000..e062e69
--- /dev/null
+++ b/tests/Feature/ListTokensCommandTest.php
@@ -0,0 +1,13 @@
+create();
+ $token2 = Token::factory()->create();
+
+ $this->artisan('ote:list-tokens')
+ ->expectsOutputToContain($token1->text)
+ ->expectsOutputToContain($token2->text)
+ ->assertExitCode(0);
+});
diff --git a/tests/Feature/ShowEntryCommandTest.php b/tests/Feature/ShowEntryCommandTest.php
new file mode 100644
index 0000000..72db7a3
--- /dev/null
+++ b/tests/Feature/ShowEntryCommandTest.php
@@ -0,0 +1,28 @@
+create();
+ $attribute = Attribute::factory()->create(['lexical_entry_id' => $entry->id]);
+ $link = Link::factory()->create(['source_lexical_entry_id' => $entry->id]);
+
+ $this->artisan('ote:show-entry', ['id' => $entry->id])
+ ->expectsOutput("Lexical Entry Details:")
+ ->expectsOutput(" ID: {$entry->id}")
+ ->expectsOutput(" Token: {$entry->token->text}")
+ ->expectsOutput(" Language: {$entry->language->name}")
+ ->expectsOutput("Attributes:")
+ ->expectsTable(['Key', 'Value'], [[$attribute->key, $attribute->value]])
+ ->expectsOutput("Links (Source):")
+ ->expectsTable(['Target Entry ID', 'Target Token', 'Type'], [[$link->target_lexical_entry_id, $link->targetEntry->token->text, $link->type]])
+ ->assertExitCode(0);
+});
+
+test('the show entry command handles non-existent entries', function () {
+ $this->artisan('ote:show-entry', ['id' => 999])
+ ->expectsOutput("Lexical entry with ID '999' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/ShowLanguageCommandTest.php b/tests/Feature/ShowLanguageCommandTest.php
new file mode 100644
index 0000000..fd4c975
--- /dev/null
+++ b/tests/Feature/ShowLanguageCommandTest.php
@@ -0,0 +1,29 @@
+create();
+ $language = $entry->language;
+
+ $this->artisan('ote:show-language', ['id' => $language->id])
+ ->expectsOutput("Language Details:")
+ ->expectsOutput(" ID: {$language->id}")
+ ->expectsOutput(" Code: {$language->code}")
+ ->expectsOutput(" Name: {$language->name}")
+ ->expectsOutput("Lexical Entries:")
+ ->expectsTable(
+ ['Entry ID', 'Token'],
+ [
+ [$entry->id, $entry->token->text],
+ ]
+ )
+ ->assertExitCode(0);
+});
+
+test('the show language command handles non-existent languages', function () {
+ $this->artisan('ote:show-language', ['id' => 999])
+ ->expectsOutput("Language with ID '999' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/ShowTokenCommandTest.php b/tests/Feature/ShowTokenCommandTest.php
new file mode 100644
index 0000000..40b3d5e
--- /dev/null
+++ b/tests/Feature/ShowTokenCommandTest.php
@@ -0,0 +1,28 @@
+create();
+ $token = $entry->token;
+
+ $this->artisan('ote:show-token', ['id' => $token->id])
+ ->expectsOutput("Token Details:")
+ ->expectsOutput(" ID: {$token->id}")
+ ->expectsOutput(" Text: {$token->text}")
+ ->expectsOutput("Lexical Entries:")
+ ->expectsTable(
+ ['Entry ID', 'Language'],
+ [
+ [$entry->id, $entry->language->name],
+ ]
+ )
+ ->assertExitCode(0);
+});
+
+test('the show token command handles non-existent tokens', function () {
+ $this->artisan('ote:show-token', ['id' => 999])
+ ->expectsOutput("Token with ID '999' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/StatsCommandTest.php b/tests/Feature/StatsCommandTest.php
new file mode 100644
index 0000000..209664b
--- /dev/null
+++ b/tests/Feature/StatsCommandTest.php
@@ -0,0 +1,26 @@
+count(5)->create();
+ Language::factory()->count(3)->create();
+ LexicalEntry::factory()->count(10)->create();
+ Attribute::factory()->count(15)->create();
+ Link::factory()->count(20)->create();
+
+ $this->artisan('ote:stats')
+ ->expectsOutputToContain('Lexicon Statistics')
+ ->expectsOutputToContain('Tokens')
+ ->expectsOutputToContain('Languages')
+ ->expectsOutputToContain('Lexical Entries')
+ ->expectsOutputToContain('Attributes')
+ ->expectsOutputToContain('Links')
+ ->expectsOutputToContain('Entries per language')
+ ->assertExitCode(0);
+});
diff --git a/tests/Feature/TokenControllerTest.php b/tests/Feature/TokenControllerTest.php
index ce87f9c..879a342 100644
--- a/tests/Feature/TokenControllerTest.php
+++ b/tests/Feature/TokenControllerTest.php
@@ -1,6 +1,7 @@
count(3)->create();
@@ -40,3 +41,14 @@
$response->assertRedirect('/tokens');
$this->assertDatabaseMissing('tokens', ['id' => $token->id]);
});
+
+test('it shows a token and its lexical entries', function () {
+ $entry = LexicalEntry::factory()->create();
+ $token = $entry->token;
+
+ $response = $this->get('/tokens/'.$token->id);
+
+ $response->assertStatus(200);
+ $response->assertSee($token->text);
+ $response->assertSee($entry->language->name);
+});
diff --git a/tests/Feature/UpdateLanguageCommandTest.php b/tests/Feature/UpdateLanguageCommandTest.php
new file mode 100644
index 0000000..01a1d08
--- /dev/null
+++ b/tests/Feature/UpdateLanguageCommandTest.php
@@ -0,0 +1,28 @@
+create(['name' => 'Old Name']);
+
+ $this->artisan('ote:update-language', [
+ 'id' => $language->id,
+ 'new_name' => 'New Name'
+ ])
+ ->expectsOutput("Language with ID '{$language->id}' has been updated to 'New Name'.")
+ ->assertExitCode(0);
+
+ $this->assertDatabaseHas('languages', [
+ 'id' => $language->id,
+ 'name' => 'New Name',
+ ]);
+});
+
+test('the update language command handles non-existent languages', function () {
+ $this->artisan('ote:update-language', [
+ 'id' => 999,
+ 'new_name' => 'New Name'
+ ])
+ ->expectsOutput("Language with ID '999' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/UpdateTokenCommandTest.php b/tests/Feature/UpdateTokenCommandTest.php
new file mode 100644
index 0000000..5b11ca5
--- /dev/null
+++ b/tests/Feature/UpdateTokenCommandTest.php
@@ -0,0 +1,28 @@
+create(['text' => 'old-text']);
+
+ $this->artisan('ote:update-token', [
+ 'id' => $token->id,
+ 'new_text' => 'new-text'
+ ])
+ ->expectsOutput("Token with ID '{$token->id}' has been updated to 'new-text'.")
+ ->assertExitCode(0);
+
+ $this->assertDatabaseHas('tokens', [
+ 'id' => $token->id,
+ 'text' => 'new-text',
+ ]);
+});
+
+test('the update token command handles non-existent tokens', function () {
+ $this->artisan('ote:update-token', [
+ 'id' => 999,
+ 'new_text' => 'new-text'
+ ])
+ ->expectsOutput("Token with ID '999' not found.")
+ ->assertExitCode(1);
+});
diff --git a/tests/Feature/ValidateCommandTest.php b/tests/Feature/ValidateCommandTest.php
new file mode 100644
index 0000000..472a25c
--- /dev/null
+++ b/tests/Feature/ValidateCommandTest.php
@@ -0,0 +1,47 @@
+artisan('ote:validate')
+ ->expectsOutput('Starting validation...')
+ ->expectsOutput('Validation complete. No issues found.')
+ ->assertExitCode(0);
+});
+
+test('the validate command finds duplicate tokens', function () {
+ $token1 = Token::factory()->create(['text' => 'apple']);
+ $token2 = Token::factory()->create(['text' => 'Apple']);
+
+ $this->artisan('ote:validate')
+ ->expectsOutputToContain('Starting validation...')
+ ->expectsOutputToContain('Found case-insensitive duplicate tokens:')
+ ->expectsOutputToContain('Apple')
+ ->expectsOutputToContain('apple')
+ ->expectsOutputToContain('Validation complete. Issues found.')
+ ->assertExitCode(0);
+});
+
+test('the validate command finds unused tokens', function () {
+ $token = Token::factory()->create();
+
+ $this->artisan('ote:validate')
+ ->expectsOutput('Starting validation...')
+ ->expectsOutput('Found unused tokens:')
+ ->expectsTable(['ID', 'Text'], [[$token->id, $token->text]])
+ ->expectsOutput('Validation complete. Issues found.')
+ ->assertExitCode(0);
+});
+
+test('the validate command finds unused languages', function () {
+ $language = Language::factory()->create();
+
+ $this->artisan('ote:validate')
+ ->expectsOutput('Starting validation...')
+ ->expectsOutput('Found unused languages:')
+ ->expectsTable(['ID', 'Name'], [[$language->id, $language->name]])
+ ->expectsOutput('Validation complete. Issues found.')
+ ->assertExitCode(0);
+});
diff --git a/tests/Pest.php b/tests/Pest.php
index fac6863..6ceca99 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -1,5 +1,10 @@
in('Browser');
+
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;